Skip to content

Commit 14fec04

Browse files
authored
ASA file_copy support transfer to peer (#200)
1 parent d45df06 commit 14fec04

File tree

3 files changed

+324
-82
lines changed

3 files changed

+324
-82
lines changed

pyntc/devices/asa_device.py

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from typing import List, Dict, Union, Optional, Iterable
1111
from ipaddress import ip_address, IPv4Address, IPv6Address, IPv4Interface, IPv6Interface
1212

13-
from netmiko import ConnectHandler, FileTransfer
14-
from netmiko.cisco import CiscoAsaSSH
13+
from netmiko import ConnectHandler
14+
from netmiko.cisco import CiscoAsaSSH, CiscoAsaFileTransfer
1515

1616
from pyntc.utils import get_structured_data
1717
from .base_device import BaseDevice, fix_docs
@@ -61,11 +61,35 @@ def _enter_config(self):
6161
self.enable()
6262
self.native.config_mode()
6363

64-
def _file_copy_instance(self, src, dest=None, file_system="flash:"):
64+
def _file_copy(self, src: str, dest: str, file_system: str) -> None:
65+
self.enable()
66+
67+
if not self.file_copy_remote_exists(src, dest, file_system):
68+
fc: CiscoAsaFileTransfer = self._file_copy_instance(src, dest, file_system)
69+
70+
try:
71+
fc.establish_scp_conn()
72+
fc.transfer_file()
73+
# Allow EOFErrors to be caught and only raise an error if the file is not actually on the device
74+
except EOFError:
75+
self.open()
76+
except Exception:
77+
raise FileTransferError
78+
finally:
79+
fc.close_scp_chan()
80+
81+
if not self.file_copy_remote_exists(src, dest, file_system):
82+
raise FileTransferError(
83+
message="Attempted file copy, but could not validate file existed after transfer"
84+
)
85+
86+
def _file_copy_instance(
87+
self, src: str, dest: Optional[str] = None, file_system: str = "flash:"
88+
) -> CiscoAsaFileTransfer:
6589
if dest is None:
6690
dest = os.path.basename(src)
6791

68-
fc = FileTransfer(self.native, src, dest, file_system=file_system)
92+
fc = CiscoAsaFileTransfer(self.native, src, dest, file_system=file_system)
6993
return fc
7094

7195
def _get_file_system(self):
@@ -355,34 +379,86 @@ def enable(self):
355379
if self.native.check_config_mode():
356380
self.native.exit_config_mode()
357381

382+
def enable_scp(self) -> None:
383+
"""
384+
Enable SCP on device by configuring "ssh scopy enable".
385+
386+
The command is ran on the active device; if the device is
387+
currently standby, then a new connection is created to the
388+
active device. The configuration is saved after to sync to peer.
389+
390+
Raises:
391+
FileTransferError: When unable to configure scopy on the active device.
392+
393+
Example:
394+
>>> device = ASADevice(**connection_args)
395+
>>> device.show("show run ssh | i scopy")
396+
''
397+
>>> device.enable_scp()
398+
>>> device.show("show run ssh | i scopy")
399+
'ssh scopy enable'
400+
>>>
401+
"""
402+
if self.is_active():
403+
device: ASADevice = self
404+
else:
405+
device = self.peer_device
406+
407+
if not device.is_active():
408+
raise FileTransferError("Unable to establish a connection with the active device")
409+
410+
try:
411+
device.config("ssh scopy enable")
412+
except CommandError:
413+
raise FileTransferError("Unable to enable scopy on the device")
414+
device.save()
415+
358416
@property
359417
def facts(self):
360418
"""Implement this once facts' re-factor is done. """
361419
return {}
362420

363-
def file_copy(self, src, dest=None, file_system=None):
364-
self.enable()
365-
if file_system is None:
366-
file_system = self._get_file_system()
421+
def file_copy(
422+
self,
423+
src: str,
424+
dest: Optional[str] = None,
425+
file_system: Optional[str] = None,
426+
peer: Optional[bool] = False,
427+
) -> None:
428+
"""
429+
Copy ``src`` file to device.
367430
368-
if not self.file_copy_remote_exists(src, dest, file_system):
369-
fc = self._file_copy_instance(src, dest, file_system=file_system)
370-
# if not self.fc.verify_space_available():
371-
# raise FileTransferError('Not enough space available.')
431+
The ``src`` file can be copied to both the device and its peer by
432+
setting ``peer`` to True. If transferring to the peer device, the
433+
transfer will use the address associated with the ``peer_interface``
434+
from "show failover" output.
372435
373-
try:
374-
fc.enable_scp()
375-
fc.establish_scp_conn()
376-
fc.transfer_file()
377-
except: # noqa E722
378-
raise FileTransferError
379-
finally:
380-
fc.close_scp_chan()
436+
Args:
437+
src (str): The path to the file to be copied to the device.
438+
dest (str): The name to use for storing the file on the device.
439+
Default is to use the name of the ``src`` file.
440+
file_system (str): The directory to store the file on the device.
441+
Default will use ``_get_file_system()`` to determine the default file_system.
442+
peer (bool): Whether to transfer the ``src`` file to the peer device.
381443
382-
if not self.file_copy_remote_exists(src, dest, file_system):
383-
raise FileTransferError(
384-
message="Attempted file copy, but could not validate file existed after transfer"
385-
)
444+
Raises:
445+
FileTransferError: When the ``src`` file is unable to transfer the file to any device.
446+
447+
Example:
448+
>>> dev = ASADevice(**connection_args)
449+
>>> dev.file_copy("path/to/asa-image.bin", peer=True)
450+
"""
451+
if dest is None:
452+
dest = os.path.basename(src)
453+
454+
if file_system is None:
455+
file_system = self._get_file_system()
456+
457+
# netmiko's enable_scp
458+
self.enable_scp()
459+
self._file_copy(src, dest, file_system)
460+
if peer:
461+
self.peer_device._file_copy(src, dest, file_system)
386462

387463
# TODO: Make this an internal method since exposing file_copy should be sufficient
388464
def file_copy_remote_exists(self, src, dest=None, file_system=None):

test/unit/test_devices/device_mocks/asa/show_failover_groups_active_active.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ slot 0: ASA-5545 hw/sw rev (1.0/7.0(0)79) status (Up Sys)
1919
admin Interface outside (10.132.8.5): Normal
2020
admin Interface folink (10.132.9.5/fe80::2a0:c9ff:fe03:101): Normal
2121
admin Interface inside (10.130.8.5/fe80::2a0:c9ff:fe01:101): Normal
22-
admin Interface fourth (10.130.9.5/fe80::3eff:fe11:6670): Normal
22+
admin Interface fourth (fe80::3eff:fe11:6670): Normal
2323
ctx1 Interface outside (10.1.1.1): Normal
2424
ctx1 Interface inside (10.2.2.1): Normal
2525
ctx2 Interface outside (10.3.3.2): Normal
@@ -35,7 +35,7 @@ slot 0: ASA-5545 hw/sw rev (1.0/7.0(0)79) status (Up Sys)
3535
admin Interface outside (10.132.8.6): Normal
3636
admin Interface folink (10.132.9.6/fe80::2a0:c9ff:fe03:102): Normal
3737
admin Interface inside (10.130.8.6/fe80::2a0:c9ff:fe01:102): Normal
38-
admin Interface fourth (10.130.9.6/fe80::3eff:fe11:6671): Normal
38+
admin Interface fourth (fe80::3eff:fe11:6671): Normal
3939
ctx1 Interface outside (10.1.1.2): Normal
4040
ctx1 Interface inside (10.2.2.2): Normal
4141
ctx2 Interface outside (10.3.3.1): Normal

0 commit comments

Comments
 (0)