Skip to content

Commit

Permalink
Merge pull request #427 from sanfrancrisko/IAC-1266/main/cisco_cfg_ba…
Browse files Browse the repository at this point in the history
…ckup_restore

(FEAT) Add Bolt tasks to backup and restore Cisco device cfgs
  • Loading branch information
DavidS authored and pmcmaw committed May 13, 2021
2 parents c6c8020 + 9afbfe6 commit 2b153c8
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 16 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ The `--modulepath` param can be retrieved by typing `puppet config print modulep
### Type

Add new types to the type directory.
We use the [Resource API format](https://github.com/puppetlabs/puppet-resource_api/blob/main/README.md)
Use the bundled ios_config example for guidance. Here is a simple example:

```Ruby
Expand Down
41 changes: 39 additions & 2 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ and restarts the puppetserver service to activate.

* [`cli_command`](#cli_command): Execute CLI Command
* [`config_save`](#config_save): Save running-config to startup-config
* [`restore_startup`](#restore_startup): Copys the startup-config to the running-config
* [`restore_startup`](#restore_startup): Copies the startup-config to the running-config
* [`backup_config`](#backup_config): Backs up the running config on the device to a given file location
* [`restore_config`](#restore_config): Restores the config from the specified location to the device

## Classes

Expand Down Expand Up @@ -2061,7 +2063,42 @@ Save running-config to startup-config

### restore_startup

Copys the startup-config to the running-config
Copies the startup-config to the running-config

**Supports noop?** false

### backup_config

Backs up the running config from a device to the specified location

#### Parameters

##### `backup_location`

Data type: `String[1]`

Location to save the running config to

##### `raw`

Data type: `Boolean`

Whether to return the raw output or wrap it into JSON

### restore_config

Restores the configuration from a given backup location to the device

#### Parameters

##### `backup_location`

Data type: `String[1]`

Location of the config to restore to the device

##### `raw`

Data type: `Boolean`

Whether to return the raw output or wrap it into JSON
8 changes: 8 additions & 0 deletions lib/puppet/transport/cisco_ios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,14 @@ def run_command_enable_mode(command)
send_command(connection, command, true)
end

def restore_config_conf_t_mode(conf)
re_conf_t = Regexp.new(%r{#{commands['default']['config_prompt']}})
run_command_enable_mode({ 'String' => 'conf t', 'Match' => re_conf_t })
conf.each do |c|
send_command(connection, "#{c}\n")
end
end

def run_command_conf_t_mode(command)
re_conf_t = Regexp.new(%r{#{commands['default']['config_prompt']}})
conf_t_cmd = { 'String' => 'conf t', 'Match' => re_conf_t }
Expand Down
2 changes: 0 additions & 2 deletions manifests/install/agent.pp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
# include cisco_ios::install::agent
class cisco_ios::install::agent {

include resource_api::install

package { 'net-ssh-telnet':
ensure => present,
provider => 'puppet_gem',
Expand Down
1 change: 0 additions & 1 deletion manifests/install/server.pp
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@
# @example Declaring the class
# include cisco_ios::install::server
class cisco_ios::install::server {
include resource_api::install::server
}
2 changes: 0 additions & 2 deletions manifests/proxy.pp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
# @note Deprecated, use cisco_ios::install::agent
class cisco_ios::proxy {

include resource_api::agent

package { 'net-ssh-telnet':
ensure => present,
provider => 'puppet_gem',
Expand Down
1 change: 0 additions & 1 deletion manifests/server.pp
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
#
# @note Deprecated, use cisco_ios::install::server
class cisco_ios::server {
include resource_api::server
}
56 changes: 56 additions & 0 deletions spec/acceptance/task_backup_restore_config_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'spec_helper_acceptance'
require 'tempfile'
require 'securerandom'

SNMP_SERVER_CONTACT = "Acceptance Test Runner #{SecureRandom.hex}".freeze
SNMP_SERVER_CONTACT_VERIFY_CMD = 'show run | inc snmp-server contact'.freeze

def create_snmp_server_contact_config
new_config_file_path = new_tempfile
File.open(new_config_file_path, 'wb') do |file|
file.write("snmp-server contact #{SNMP_SERVER_CONTACT}\n")
file.write("end\n")
end
new_config_file_path
end

def snmp_server_contact_set
output, status = Open3.capture2e(bolt_task_command('cli_command',
"command=\"#{SNMP_SERVER_CONTACT_VERIFY_CMD}\"",
'raw=false'))
if status.success?
output.split("\n").each do |line|
return true if line.include? SNMP_SERVER_CONTACT_VERIFY_CMD
end
end
false
end

describe 'bolt task to backup / restore config' do
before(:all) do
unless ENV['SKIP_STARTUP_RESTORE']
# Restore the running config back to the startup config
_output, status = Open3.capture2e(bolt_task_command('restore_startup'))
raise 'Error restoring startup config on target' unless status.success?
end
end

it 'can backup a running config' do
backup_location = new_tempfile
_output, status = Open3.capture2e(bolt_task_command('backup_config', "backup_location=#{backup_location}"))
expect(status.success?).to be true
expect(File.empty?(backup_location)).to be false
end

it 'can restore a config' do
# Create a simple config that sets the 'snmp server contact' parameter
snmp_server_contact_config = create_snmp_server_contact_config
_output, status = Open3.capture2e(bolt_task_command('restore_config', "backup_location=#{snmp_server_contact_config}"))
expect(status.success?).to be true

# Query the 'snmp server contact' parameter and ensure it has been set to the expected value
expect(snmp_server_contact_set).to be true
end
end
35 changes: 28 additions & 7 deletions spec/spec_helper_acceptance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ def self.device_xe?
end
end

def bolt_task_command(task, *opts)
"BOLT_GEM=true bundle exec bolt task run cisco_ios::#{task} --modulepath spec/fixtures/modules --targets sut " \
"--inventoryfile spec/fixtures/inventory.yml #{opts.join(' ')}".strip
end

def new_tempfile
(@tempfiles ||= []) << Tempfile.new
@tempfiles.last.path
end

RSpec.configure do |c|
c.before :suite do
system('rake spec_prep')
Expand Down Expand Up @@ -117,12 +127,16 @@ def self.device_xe?

File.open('spec/fixtures/inventory.yml', 'w') do |file|
file.puts <<CREDENTIALS
nodes:
- name: #{RSpec.configuration.host}
alias: sut
config:
transport: remote
remote:
---
version: 2
groups:
- name: #{RSpec.configuration.host.split('.')[0]}
targets:
- uri: #{RSpec.configuration.host}
alias: sut
config:
transport: remote
remote:
remote-transport: cisco_ios
user: #{RSpec.configuration.user}
password: #{RSpec.configuration.password}
Expand All @@ -133,7 +147,7 @@ def self.device_xe?
# do not provision if forbidden
unless ENV['BEAKER_provision'] == 'no'
# reset the device to it's startup-config
result = Open3.capture2e('bundle exec bolt task run cisco_ios::restore_startup --modulepath spec/fixtures/modules --nodes sut --inventoryfile spec/fixtures/inventory.yml')
result = Open3.capture2e('BOLT_GEM=true bundle exec bolt task run cisco_ios::restore_startup --modulepath spec/fixtures/modules --targets sut --inventoryfile spec/fixtures/inventory.yml')
# result = Open3.capture2e("bundle exec bolt task show --modulepath ../")
puts result

Expand Down Expand Up @@ -184,4 +198,11 @@ def self.device_xe?
run_device(allow_changes: true)
end
end
c.after :suite do
unless ENV['SKIP_STARTUP_RESTORE']
# Restore the running config back to the startup config after the suite has completed
_output, status = Open3.capture2e(bolt_task_command('restore_startup'))
raise 'Error restoring startup config on target' unless status.success?
end
end
end
20 changes: 20 additions & 0 deletions tasks/backup_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"description": "Backup the Cisco IOS running config to a file",
"input_method": "stdin",
"remote": true,
"parameters": {
"backup_location": {
"description": "File to save running config",
"type": "String[1]"
}
},
"files": [
"cisco_ios/lib/puppet/util/task_helper.rb",
"cisco_ios/lib/puppet/transport/cisco_ios.rb",
"cisco_ios/lib/puppet/transport/command.yaml",
"cisco_ios/lib/puppet/transport/schema/cisco_ios.rb",
"cisco_ios/lib/puppet_x/puppetlabs/cisco_ios/utility.rb",
"cisco_ios/lib/puppet_x/puppetlabs/cisco_ios/transport_shim.rb",
"ruby_task_helper/files/task_helper.rb"
]
}
32 changes: 32 additions & 0 deletions tasks/backup_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/opt/puppetlabs/puppet/bin/ruby
# frozen_string_literal: true

require_relative '../lib/puppet/util/task_helper'
task = Puppet::Util::TaskHelper.new('cisco_ios')

result = {}

unless Puppet.settings.global_defaults_initialized?
Puppet.initialize_settings
end

begin
rtn = task.transport.run_command_enable_mode('show running-config')
File.open(task.params['backup_location'], 'wb') do |f|
f.write "#{rtn}\n"
end
result = {
backup_location: task.params['backup_location'],
}
rescue StandardError => e
result[:_error] = {
msg: e.message,
kind: 'puppetlabs/cisco_ios',
details: {
class: e.class.to_s,
backtrace: e.backtrace,
},
}
end

puts result.to_json
20 changes: 20 additions & 0 deletions tasks/restore_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"description": "Restore the config to a Cisco IOS device",
"input_method": "stdin",
"remote": true,
"parameters": {
"backup_location": {
"description": "File to restore to device",
"type": "String[1]"
}
},
"files": [
"cisco_ios/lib/puppet/util/task_helper.rb",
"cisco_ios/lib/puppet/transport/cisco_ios.rb",
"cisco_ios/lib/puppet/transport/command.yaml",
"cisco_ios/lib/puppet/transport/schema/cisco_ios.rb",
"cisco_ios/lib/puppet_x/puppetlabs/cisco_ios/utility.rb",
"cisco_ios/lib/puppet_x/puppetlabs/cisco_ios/transport_shim.rb",
"ruby_task_helper/files/task_helper.rb"
]
}
60 changes: 60 additions & 0 deletions tasks/restore_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/opt/puppetlabs/puppet/bin/ruby
# frozen_string_literal: true

require_relative '../lib/puppet/util/task_helper'
task = Puppet::Util::TaskHelper.new('cisco_ios')

CONFIG_LINE_HEADER_CUTOFF = 20
CONFIG_LINE_ENDING_ID = 'end'.freeze

result = {}

unless Puppet.settings.global_defaults_initialized?
Puppet.initialize_settings
end

VALID_CISCO_HEADERS = ['show running-config', 'Building configuration', 'Current configuration'].freeze

def config_to_restore(raw_config_output)
header_match_count = 0
line = 0
start_line = nil
end_line = nil

raw_config_output.each do |current_line|
header_match_count += 1 if VALID_CISCO_HEADERS.any? { |vch| current_line.start_with? vch }
start_line = (line + 1) if (header_match_count == 3) && (current_line.start_with? 'version')
end_line = line if current_line.start_with? CONFIG_LINE_ENDING_ID
break if start_line && end_line
# If we haven't found the headers by line 20, let's exit as this doesn't seem to be a valid Cisco
# backup config retrieved using 'show running-config'
if line >= CONFIG_LINE_HEADER_CUTOFF && header_match_count < 3
raise "Did not detect the following expected headers expected from a valid Cisco backup after processing #{CONFIG_LINE_HEADER_CUTOFF} lines:\n" +
VALID_CISCO_HEADERS.join("\n")
end
line += 1
end

raise "Could not determine end of config (was expecting '#{CONFIG_LINE_ENDING_ID}')" unless end_line
raw_config_output[start_line..end_line].map { |l| l.strip }
end

begin
config_to_restore = config_to_restore(File.readlines(task.params['backup_location']))
task.transport.restore_config_conf_t_mode(config_to_restore)

result = {
last_config_change: '',
}
rescue StandardError => e
result[:_error] = {
msg: e.message,
kind: 'puppetlabs/cisco_ios',
details: {
class: e.class.to_s,
backtrace: e.backtrace,
},
}
end

puts result.to_json

0 comments on commit 2b153c8

Please sign in to comment.