Skip to content

Commit a069576

Browse files
authored
INSTALL OS: Add method that performs install of OS (#69)
* `install_os` method added to all but `JunosDevice`. - File(s) must be on the device already. - Checks that image is not already booted on the device. - Ensures device is ready to install image. - Reboots the device if required for OS installation (F5Device does not) - Waits for device to reboot or finish installation. - Raises `OSInstallError if image is not confirmed installed. - Returns True if image was installed. - Returns False if device was already booted on the device. * F5Device: - Double default timeout value for image installation. - Implement hardened _wait_for_image_installed() method - Add NTCFileNotFoundError exception in case of attempts to install non-existing image file. * NXOSDevice: - Change Exception output for kickstart's `NTCFileNotFound` to use kickstart file instead of system file. * ASADevice: - Add call to `save` method in `set_boot_options` to mimic behavior of `IOSDevice` and potentially avoid bug that was discovered there. * BaseDevice: - Update `install_os` docstring with updates required for method to actually be implemented.
1 parent 4da4f46 commit a069576

File tree

7 files changed

+131
-12
lines changed

7 files changed

+131
-12
lines changed

pyntc/devices/asa_device.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,18 @@
99
from netmiko import ConnectHandler
1010
from netmiko import FileTransfer
1111

12-
12+
from pyntc.templates import get_structured_data
13+
from .base_device import BaseDevice, fix_docs
14+
from .system_features.file_copy.base_file_copy import FileTransferError
1315
from pyntc.errors import (
1416
CommandError,
1517
CommandListError,
1618
FileSystemNotFoundError,
1719
NTCError,
1820
NTCFileNotFoundError,
1921
RebootTimeoutError,
22+
OSInstallError,
2023
)
21-
from pyntc.templates import get_structured_data
22-
from .base_device import BaseDevice, fix_docs
23-
from .system_features.file_copy.base_file_copy import FileTransferError
24-
2524

2625
@fix_docs
2726
class ASADevice(BaseDevice):
@@ -243,6 +242,19 @@ def get_boot_options(self):
243242

244243
return dict(sys=boot_image)
245244

245+
def install_os(self, image_name, **vendor_specifics):
246+
timeout = vendor_specifics.get("timeout", 3600)
247+
if not self._image_booted(image_name):
248+
self.set_boot_options(image_name, **vendor_specifics)
249+
self.reboot(confirm=True)
250+
self._wait_for_device_reboot(timeout=timeout)
251+
if not self._image_booted(image_name):
252+
raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=image_name)
253+
254+
return True
255+
256+
return False
257+
246258
def open(self):
247259
if self._connected:
248260
try:
@@ -314,14 +326,16 @@ def set_boot_options(self, image_name, **vendor_specifics):
314326
file_system_files = self.show("dir {0}".format(file_system))
315327
if re.search(image_name, file_system_files) is None:
316328
raise NTCFileNotFoundError(
317-
hostname=self.facts.get("hostname"), file=image_name, dir=file_system
329+
# TODO: Update to use hostname
330+
hostname=self.host, file=image_name, dir=file_system
318331
)
319332

320333
current_images = current_boot.splitlines()
321334
commands_to_exec = ["no {0}".format(image) for image in current_images]
322335
commands_to_exec.append("boot system {0}/{1}".format(file_system, image_name))
323336
self.config_list(commands_to_exec)
324337

338+
self.save()
325339
if self.get_boot_options()["sys"] != image_name:
326340
raise CommandError(
327341
command="boot system {0}/{1}".format(file_system, image_name),

pyntc/devices/base_device.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,39 @@ def get_boot_options(self):
191191
"""
192192
raise NotImplementedError
193193

194+
@abc.abstractmethod
195+
def install_os(self, image_name, **vendor_specifics):
196+
"""Install the OS from specified image_name
197+
198+
Args:
199+
image_name(str): The name of the image on the device to install.
200+
201+
Keyword Args:
202+
kickstart (str): Option for ``NXOSDevice`` for devices that require a kickstart image.
203+
volume (str): Option for ``F5Device`` to set the target boot volume.
204+
file_system (str): Option for ``ASADevice``, ``EOSDevice``, ``IOSDevice``, and
205+
``NXOSDevice`` to set where the OS files are stored. The default will use
206+
the ``_get_file_system`` method.
207+
timeout (int): Option for ``IOSDevice`` and ``NXOSDevice`` to set the wait time for
208+
device installation to complete.
209+
210+
Returns:
211+
True if system has been installed during function's call, False if OS has not been installed
212+
213+
Raises:
214+
OSInstallError: When device finishes installation process, but the running image
215+
does not match ``image_name``.
216+
CommandError: When sending a command to the device fails, or when the config status
217+
after sending a command does not yield expected results.
218+
CommandListError: When sending commands to the device fails.
219+
NotEnoughFreeSpaceError: When the device does not have enough free space for install.
220+
NTCFileNotFoundError: When the ``image_name`` is not found in the devices ``file_system``.
221+
FileSystemNotFoundError: When the ``file_system`` is left to default,
222+
and the ``file_system`` cannot be identified.
223+
RebootTimeoutError: When device is rebooted and is unreachable longer than ``timeout`` period.
224+
"""
225+
raise NotImplementedError
226+
194227
@abc.abstractmethod
195228
def open(self):
196229
"""Open a connection to the device.

pyntc/devices/eos_device.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
NTCError,
1818
NTCFileNotFoundError,
1919
RebootTimeoutError,
20+
OSInstallError,
2021
)
2122

2223
from pyeapi import connect as eos_connect
@@ -180,6 +181,19 @@ def get_boot_options(self):
180181
image = image.replace('flash:', '')
181182
return dict(sys=image)
182183

184+
def install_os(self, image_name, **vendor_specifics):
185+
timeout = vendor_specifics.get("timeout", 3600)
186+
if not self._image_booted(image_name):
187+
self.set_boot_options(image_name, **vendor_specifics)
188+
self.reboot(confirm=True)
189+
self._wait_for_device_reboot(timeout=timeout)
190+
if not self._image_booted(image_name):
191+
raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=image_name)
192+
193+
return True
194+
195+
return False
196+
183197
def open(self):
184198
pass
185199

pyntc/devices/f5_device.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
import requests
1111
from f5.bigip import ManagementRoot
1212

13+
from pyntc.errors import NotEnoughFreeSpaceError, OSInstallError, \
14+
NTCFileNotFoundError
1315
from .base_device import BaseDevice
1416
from .system_features.file_copy.base_file_copy import FileTransferError
15-
from pyntc.errors import NotEnoughFreeSpaceError, OSInstallError
1617

1718

1819
class F5Device(BaseDevice):
@@ -338,7 +339,7 @@ def _wait_for_device_reboot(self, volume_name, timeout=600):
338339
pass
339340
return False
340341

341-
def _wait_for_image_installed(self, image_name, volume, timeout=900):
342+
def _wait_for_image_installed(self, image_name, volume, timeout=1800):
342343
"""Waits for the device to install image on a volume
343344
344345
Args:
@@ -353,8 +354,13 @@ def _wait_for_image_installed(self, image_name, volume, timeout=900):
353354

354355
while time.time() < end_time:
355356
time.sleep(20)
356-
if self.image_installed(image_name=image_name, volume=volume):
357-
return
357+
# Avoid race-conditions issues. Newly created volumes _might_ lack
358+
# of .version attribute in first seconds of their live.
359+
try:
360+
if self.image_installed(image_name=image_name, volume=volume):
361+
return
362+
except:
363+
pass
358364

359365
raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=volume)
360366

@@ -395,7 +401,6 @@ def file_copy(self, src, dest=None, **kwargs):
395401
if not self.file_copy_remote_exists(src, dest, **kwargs):
396402
self._check_free_space(min_space=6)
397403
self._upload_image(image_filepath=src)
398-
399404
if not self.file_copy_remote_exists(src, dest, **kwargs):
400405
raise FileTransferError(
401406
message="Attempted file copy, "
@@ -446,6 +451,22 @@ def image_installed(self, image_name, volume):
446451

447452
return False
448453

454+
def install_os(self, image_name, **vendor_specifics):
455+
volume = vendor_specifics.get('volume')
456+
if not self.image_installed(image_name, volume):
457+
self._check_free_space(min_space=6)
458+
if not self._image_exists(image_name):
459+
raise NTCFileNotFoundError(
460+
hostname=self._get_hostname(), file=image_name,
461+
dir='/shared/images'
462+
)
463+
self._image_install(image_name=image_name, volume=volume)
464+
self._wait_for_image_installed(image_name=image_name, volume=volume)
465+
466+
return True
467+
468+
return False
469+
449470
def open(self):
450471
pass
451472

@@ -475,6 +496,11 @@ def save(self, filename=None):
475496
def set_boot_options(self, image_name, **vendor_specifics):
476497
volume = vendor_specifics.get('volume')
477498
self._check_free_space(min_space=6)
499+
if not self._image_exists(image_name):
500+
raise NTCFileNotFoundError(
501+
hostname=self._get_hostname(), file=image_name,
502+
dir='/shared/images'
503+
)
478504
self._image_install(image_name=image_name, volume=volume)
479505
self._wait_for_image_installed(image_name=image_name, volume=volume)
480506

pyntc/devices/ios_device.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
FileSystemNotFoundError,
1818
NTCError,
1919
NTCFileNotFoundError,
20+
OSInstallError,
2021
RebootTimeoutError,
2122
)
2223

@@ -271,6 +272,19 @@ def get_boot_options(self):
271272

272273
return {'sys': boot_image}
273274

275+
def install_os(self, image_name, **vendor_specifics):
276+
timeout = vendor_specifics.get("timeout", 3600)
277+
if not self._image_booted(image_name):
278+
self.set_boot_options(image_name, **vendor_specifics)
279+
self.reboot(confirm=True)
280+
self._wait_for_device_reboot(timeout=timeout)
281+
if not self._image_booted(image_name):
282+
raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=image_name)
283+
284+
return True
285+
286+
return False
287+
274288
def open(self):
275289
if self._connected:
276290
try:

pyntc/devices/jnpr_device.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs):
189189
def get_boot_options(self):
190190
return self.facts['os_version']
191191

192+
def install_os(self, image_name, **vendor_specifics):
193+
raise NotImplementedError
194+
192195
def open(self):
193196
if not self.connected:
194197
self.native.open()

pyntc/devices/nxos_device.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
CommandListError,
1313
NTCFileNotFoundError,
1414
RebootTimeoutError,
15+
OSInstallError,
1516
)
1617

1718
from pynxos.device import Device as NXOSNative
@@ -102,6 +103,19 @@ def file_copy_remote_exists(self, src, dest=None, file_system='bootflash:'):
102103
def get_boot_options(self):
103104
return self.native.get_boot_options()
104105

106+
def install_os(self, image_name, **vendor_specifics):
107+
timeout = vendor_specifics.get("timeout", 3600)
108+
if not self._image_booted(image_name):
109+
self.set_boot_options(image_name, **vendor_specifics)
110+
self._wait_for_device_reboot(timeout=timeout)
111+
if not self._image_booted(image_name):
112+
raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=image_name)
113+
self.save()
114+
115+
return True
116+
117+
return False
118+
105119
def open(self):
106120
pass
107121

@@ -134,10 +148,11 @@ def set_boot_options(self, image_name, kickstart=None, **vendor_specifics):
134148
raise NTCFileNotFoundError(
135149
hostname=self.facts.get("hostname"), file=image_name, dir=file_system
136150
)
151+
137152
if kickstart is not None:
138153
if re.search(kickstart, file_system_files) is None:
139154
raise NTCFileNotFoundError(
140-
hostname=self.facts.get("hostname"), file=image_name, dir=file_system
155+
hostname=self.facts.get("hostname"), file=kickstart, dir=file_system
141156
)
142157

143158
kickstart = file_system + kickstart

0 commit comments

Comments
 (0)