tl1 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+ require 'strscan'
3
+
4
+ module TL1
5
+ # A representation of a CLI interaction, including an input message format and
6
+ # an output message format.
7
+ class Command
8
+ attr_reader :input_format, :output_format
9
+
10
+ def initialize(input, output = nil)
11
+ @input_format = TL1::InputFormat.new(input)
12
+ @output_format = output && TL1::OutputFormat.new(output)
13
+ end
14
+
15
+ def input(**kwargs)
16
+ input_format.format(**kwargs) + ';'
17
+ end
18
+
19
+ def record_sources(output)
20
+ OutputScanner.new(output).records
21
+ end
22
+
23
+ def parse_output(output)
24
+ return output unless output_format
25
+
26
+ record_sources(output).map do |record_source|
27
+ output_format.parse(record_source)
28
+ end
29
+ end
30
+
31
+ # A helper class to extract records from output messages. Assumes records
32
+ # are double-quoted and separated by newlines.
33
+ class OutputScanner
34
+ attr_reader :message
35
+
36
+ def initialize(message)
37
+ @message = message
38
+ end
39
+
40
+ def records
41
+ records = []
42
+ scanner = StringScanner.new(message)
43
+ scan_begin(scanner)
44
+ loop do
45
+ record = scan_next_record(scanner)
46
+ break unless record
47
+ records << record
48
+ end
49
+ records
50
+ end
51
+
52
+ def scan_begin(scanner)
53
+ scanner.skip_until(/^M\s+\d+\s+COMPLD$/)
54
+ end
55
+
56
+ def scan_next_record(scanner)
57
+ scanner.skip(/\s*/)
58
+ char = scanner.getch
59
+ case char
60
+ when '>'
61
+ scan_begin(scanner)
62
+ scan_next_record(scanner)
63
+ when ';'
64
+ nil
65
+ when '"'
66
+ scan_record(scanner)
67
+ end
68
+ end
69
+
70
+ def scan_characters(scanner)
71
+ loop do
72
+ raise 'Unexpected end of message' if scanner.eos?
73
+ yield scanner.getch
74
+ end
75
+ end
76
+
77
+ def scan_record(scanner)
78
+ record = String.new
79
+
80
+ scan_characters(scanner) do |char|
81
+ case char
82
+ when '\\'
83
+ next_char = scanner.getch
84
+ if next_char == '"'
85
+ record << "\"#{scan_record_quoted_string(scanner)}"
86
+ else
87
+ record << char << next_char
88
+ end
89
+ when '"'
90
+ return record
91
+ else
92
+ record << char
93
+ end
94
+ end
95
+ end
96
+
97
+ def scan_record_quoted_string(scanner)
98
+ record = String.new
99
+
100
+ scan_characters(scanner) do |char|
101
+ case char
102
+ when '\\'
103
+ next_char = scanner.getch
104
+ return "#{record}\"" if next_char == '"'
105
+ record << char << next_char
106
+ else
107
+ record << char
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end # class Command
113
+ end # module TL1
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module TL1
3
+ # A format for an input message.
4
+ class InputFormat
5
+ attr_reader :source
6
+
7
+ def initialize(source)
8
+ @source = source
9
+ end
10
+
11
+ def ast
12
+ @ast ||= AST.parse_message_format(source)
13
+ end
14
+
15
+ def as_json
16
+ ast.as_json
17
+ end
18
+
19
+ def format(**kwargs)
20
+ ast.format(**kwargs)
21
+ end
22
+ end # class OutputFormat
23
+ end # module TL1
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module TL1
3
+ # A format for records appearing in output messages.
4
+ class OutputFormat
5
+ attr_reader :source
6
+
7
+ def initialize(source)
8
+ @source = source
9
+ end
10
+
11
+ def ast
12
+ @ast ||= AST.parse_message_format(source)
13
+ end
14
+
15
+ def as_json
16
+ ast.as_json
17
+ end
18
+
19
+ def parse(record_source)
20
+ ast.parse(record_source)
21
+ end
22
+ end # class OutputFormat
23
+ end # module TL1
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop: disable Metrics/ModuleLength
4
+ module TL1
5
+ module Platforms
6
+ module BTI
7
+ ACT_USER = Command.new('ACT-USER::<username>:::<password>')
8
+ CANC_USER = Command.new('CANC-USER::<username>')
9
+
10
+ RTRV_EQPT_ALL = Command.new(
11
+ 'RTRV-EQPT',
12
+ '<aid>:<type>:ID=<id>,C1=<custom1>,C2=<custom2>,C3=<custom3>:pst,sst'
13
+ )
14
+
15
+ RTRV_INV_ALL = Command.new(
16
+ 'RTRV-INV',
17
+ [
18
+ '<aid>,<aidtype>',
19
+ [
20
+ 'NAME=<name>',
21
+ 'PEC=<pec>',
22
+ 'CLEI=<clei>',
23
+ 'FNAME=<fname>',
24
+ 'SER=<ser>',
25
+ 'HWREV=<hwrev>',
26
+ 'FW=<fw>',
27
+ 'MFGDAT=<mfgdat>',
28
+ 'MFGLOCN=<mfglocn>',
29
+ 'TSTDAT=<tstdat>',
30
+ 'TSTLOCN=<tstlocn>',
31
+ 'WAVELENGTH=<wavelength>',
32
+ 'WAVELENGTHMIN=<wavelengthmin>',
33
+ 'WAVELENGTHMAX=<wavelengthmax>',
34
+ 'WAVELENGTHSPACING=<wavelengthspacing>',
35
+ 'REACH=<reach>',
36
+ 'MINBR=<minbr>',
37
+ 'MAXBR=<maxbr>',
38
+ 'NOMBR=<nombr>',
39
+ 'ENCODING=<encoding>',
40
+ 'CONNTYPE=<conntype>',
41
+ 'VENDORNAME=<vendorname>',
42
+ 'VENDORPN=<vendorpn>',
43
+ 'VENDOROUI=<vendoroui>',
44
+ 'TXFAULTIMP=<txfaultimp>',
45
+ 'TXDISABLEIMP=<txdisableimp>',
46
+ 'LOSIMP=<losimp>',
47
+ 'DDIAGIMP=<ddiagimp>',
48
+ 'MEDIA=<media>',
49
+ 'USI=<usi>',
50
+ 'TEMPHT=<tempht>',
51
+ 'TEMPHTS=<temphts>'
52
+ ].join(',')
53
+ ].join(':')
54
+ )
55
+
56
+ RTRV_ALM_ALL = Command.new(
57
+ 'RTRV-ALM-ALL',
58
+ [
59
+ '<aid>,<aidtype>',
60
+ [
61
+ '<ntfcncde>',
62
+ '<condtype>',
63
+ '<srveff>',
64
+ '<ocrdat>',
65
+ '<ocrtim>',
66
+ '<locn>',
67
+ '<dirn>',
68
+ '<tmper>'
69
+ ].join(','),
70
+ '<conddescr>,<aiddet>,<obsdbhvr>,<exptdbhvr>',
71
+ '<dgntype>,<tblislt>'
72
+ ].join(':')
73
+ )
74
+
75
+ RTRV_CRS_WCH = Command.new(
76
+ 'RTRV-CRS-WCH',
77
+ '<from_aid>,<to_aid>::SERVICENAME=<service_name>'
78
+ )
79
+
80
+ RTRV_CRS_XCVR = Command.new(
81
+ 'RTRV-CRS-XCVR',
82
+ '<src_aid>,<dst_aid>:<ctype>'
83
+ )
84
+
85
+ RTRV_CONN_EQPT = Command.new(
86
+ 'RTRV-CONN-EQPT',
87
+ '<fromAid>,<toAid>:<type>'
88
+ )
89
+
90
+ RTRV_WDM = Command.new(
91
+ 'RTRV-WDM',
92
+ [
93
+ '<aid>',
94
+ '',
95
+ [
96
+ '<ID=<id>',
97
+ 'C1=<custom1>',
98
+ 'C2=<custom2>',
99
+ 'C3=<custom3>',
100
+ 'FIBER=<fiber>',
101
+ 'SPANLEN=<spanlen>',
102
+ 'SPANLOSSSPECMAX=<spanlossspecmax>',
103
+ 'SPANLOSSRX-HT=<spanlossrx-ht>',
104
+ 'NUMCHNLS=<numchnls>',
105
+ 'AINSTMR=<ainstmr>',
106
+ 'ACTAINSTMR=<actainstmr>'
107
+ ].join(','),
108
+ '<pst>,<sst>'
109
+ ].join(':')
110
+ )
111
+
112
+ RTRV_ROUTE_CONN = Command.new(
113
+ 'RTRV-ROUTE-CONN',
114
+ [
115
+ '',
116
+ '<ipaddr>,<mask>,<nexthop>',
117
+ [
118
+ 'COST=<cost>',
119
+ 'ADMINDIST=<admindist>',
120
+ 'TYPE=<type>',
121
+ 'PROT=<prot>',
122
+ 'AGE=<age>',
123
+ 'PREFSTAT=<prefstat>'
124
+ ].join(',')
125
+ ].join(':')
126
+ )
127
+ end # module BTI
128
+ end # module Platforms
129
+ end # module TL1
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+ module TL1
3
+ # A wrapper around an IO-like object representing a connection to a
4
+ # TL1-capable network element.
5
+ class Session
6
+ # @param io [IO, Net::SSH::Telnet, Net::Telnet]
7
+ # An established connection to a TL1 server.
8
+ #
9
+ # The connection object must have a `#write` method, and one of two read
10
+ # methods: `#expect` or `#waitfor`. If you are using Net::Telnet or
11
+ # Net::SSH::Telnet, `#waitfor` will be used. Otherwise, you should make
12
+ # sure that your connection object has an `#expect` method that behaves
13
+ # like `IO#expect` from the standard library.
14
+ #
15
+ # @param timeout [Integer]
16
+ # How long to wait for responses, by default.
17
+ def initialize(io, timeout = 10)
18
+ @timeout = timeout
19
+ @io =
20
+ if io.respond_to?(:expect)
21
+ io
22
+ elsif io.respond_to?(:waitfor)
23
+ WaitforWrapper.new(io)
24
+ else
25
+ raise UnsupportedIOError,
26
+ "the given IO doesn't respond to expect or waitfor"
27
+ end
28
+ end
29
+
30
+ # Execute a TL1::Command
31
+ #
32
+ # @param [TL1::Command]
33
+ # @return [TL1::AST::Node]
34
+ def cmd(command, **kwargs)
35
+ output = raw_cmd(command.input(**kwargs))
36
+ command.parse_output(output)
37
+ end
38
+
39
+ # Receive data until the given pattern is matched.
40
+ #
41
+ # @param pattern [Regexp]
42
+ # @param timeout [Integer]
43
+ def expect(pattern, timeout = nil)
44
+ timeout ||= @timeout
45
+ @io.expect(pattern, timeout)
46
+ end
47
+
48
+ # Send a string and receive a string back.
49
+ #
50
+ # @param message [String]
51
+ # @param timeout [Integer]
52
+ # @return [String]
53
+ def raw_cmd(message, timeout = nil)
54
+ write(message)
55
+ expect(COMPLD, timeout)
56
+ end
57
+
58
+ # Send a string.
59
+ #
60
+ # @param message [String]
61
+ # @return [Boolean]
62
+ def write(message)
63
+ @io.write(message)
64
+ end
65
+
66
+ # Wraps objects that support `#waitfor` but not `#expect`, such as
67
+ # Net::Telnet and Net::SSH::Telnet. It is used transparently by
68
+ # `TL1::Session#initialize` for those classes, so it shouldn't be necessary
69
+ # to use it directly. If you are defining a new class that responds to
70
+ # `#waitfor`, you can define your own `#expect` method instead of using
71
+ # this.
72
+ class WaitforWrapper
73
+ def initialize(io)
74
+ @io = io
75
+ end
76
+
77
+ def expect(pattern, timeout)
78
+ @io.waitfor('Match' => pattern, 'Timeout' => timeout)
79
+ end
80
+
81
+ def write(*args)
82
+ @io.write(*args)
83
+ end
84
+ end # class WaitforWrapper
85
+ end # class Session
86
+ end # module TL1
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module TL1
3
+ # A simple IO-like object that accepts a hash of string inputs and
4
+ # corresponding outputs, useful for testing.
5
+ class TestIO
6
+ def initialize(commands, first_output: '')
7
+ commands.each_pair do |input, output|
8
+ next if output =~ COMPLD
9
+ raise ArgumentError, "incomplete output for #{input}"
10
+ end
11
+
12
+ @commands = commands
13
+ @next_output = first_output
14
+ end
15
+
16
+ def expect(pattern, _timeout = nil)
17
+ unless pattern =~ @next_output
18
+ raise TimeoutError, 'pattern does not match the next output'
19
+ end
20
+
21
+ @next_output
22
+ end
23
+
24
+ def write(message)
25
+ @next_output = @commands.fetch(message)
26
+ true
27
+ end
28
+
29
+ class TimeoutError < RuntimeError; end
30
+ end # class TestIO
31
+ end # module TL1
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Tl1
3
+ VERSION = '0.1.0'
4
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tl1/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'tl1'
8
+ spec.version = Tl1::VERSION
9
+ spec.authors = ['Ben Miller']
10
+ spec.email = ['bmiller@rackspace.com']
11
+
12
+ spec.summary = 'Define, send, and receive TL1 messages'
13
+ spec.homepage = 'https://github.com/bjmllr/tl1'
14
+ spec.license = 'GPL-3.0'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.14'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tl1
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Miller
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description:
56
+ email:
57
+ - bmiller@rackspace.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rubocop.yml"
64
+ - ".travis.yml"
65
+ - CODE_OF_CONDUCT.md
66
+ - Gemfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - lib/tl1.rb
73
+ - lib/tl1/ast.rb
74
+ - lib/tl1/command.rb
75
+ - lib/tl1/input_format.rb
76
+ - lib/tl1/output_format.rb
77
+ - lib/tl1/platforms/bti.rb
78
+ - lib/tl1/session.rb
79
+ - lib/tl1/test_io.rb
80
+ - lib/tl1/version.rb
81
+ - tl1.gemspec
82
+ homepage: https://github.com/bjmllr/tl1
83
+ licenses:
84
+ - GPL-3.0
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.6.11
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Define, send, and receive TL1 messages
106
+ test_files: []