tl1 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []