forked from rockstor/rockstor-core
/
osi.py
1740 lines (1624 loc) · 81.2 KB
/
osi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Copyright (c) 2012-2013 RockStor, Inc. <http://rockstor.com>
This file is part of RockStor.
RockStor is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published
by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
RockStor is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import re
import os
# TODO: consider drop in replacement of subprocess32 module
import subprocess
import signal
import collections
import shutil
from tempfile import mkstemp
import time
from socket import inet_ntoa
from struct import pack
from exceptions import CommandException, NonBTRFSRootException
import hashlib
import logging
import uuid
from django.conf import settings
logger = logging.getLogger(__name__)
MKDIR = '/bin/mkdir'
RMDIR = '/bin/rmdir'
MOUNT = '/bin/mount'
UMOUNT = '/bin/umount'
EXPORTFS = '/usr/sbin/exportfs'
HOSTID = '/usr/bin/hostid'
DEFAULT_MNT_DIR = '/mnt2/'
SHUTDOWN = '/usr/sbin/shutdown'
GRUBBY = '/usr/sbin/grubby'
CAT = '/usr/bin/cat'
UDEVADM = '/usr/sbin/udevadm'
NMCLI = '/usr/bin/nmcli'
HOSTNAMECTL = '/usr/bin/hostnamectl'
LSBLK = '/usr/bin/lsblk'
HDPARM = '/usr/sbin/hdparm'
SYSTEMCTL_BIN = '/usr/bin/systemctl'
WIPEFS = '/usr/sbin/wipefs'
DD = '/bin/dd'
LS = '/usr/bin/ls'
Disk = collections.namedtuple('Disk',
'name model serial size transport vendor '
'hctl type fstype label uuid parted root '
'partitions')
def inplace_replace(of, nf, regex, nl):
with open(of) as afo, open(nf, 'w') as tfo:
replaced = [False, ] * len(regex)
for l in afo.readlines():
ireplace = False
for i in range(0, len(regex)):
if (re.match(regex[i], l) is not None):
tfo.write(nl[i])
replaced[i] = True
ireplace = True
break
if (not ireplace):
tfo.write(l)
for i in range(0, len(replaced)):
if (not replaced[i]):
tfo.write(nl[i])
def run_command(cmd, shell=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.PIPE, throw=True,
log=False, input=None):
try:
cmd = map(str, cmd)
p = subprocess.Popen(cmd, shell=shell, stdout=stdout, stderr=stderr,
stdin=stdin)
out, err = p.communicate(input=input)
out = out.split('\n')
err = err.split('\n')
rc = p.returncode
except Exception, e:
msg = ('Exception while running command(%s): %s' % (cmd, e.__str__()))
raise Exception(msg)
if (rc != 0):
if (log):
e_msg = ('non-zero code(%d) returned by command: %s. output: %s error:'
' %s' % (rc, cmd, out, err))
logger.error(e_msg)
if (throw):
raise CommandException(cmd, out, err, rc)
return (out, err, rc)
def scan_disks(min_size):
"""
Using lsblk we scan all attached disks and categorize them according to
if they are partitioned, their file system, if the drive hosts our / mount
point etc. The result of this scan is used by:-
view/disk.py _update_disk_state
for further analysis / categorization.
N.B. if a device (partition or whole dev) hosts swap or is of no interest
then it is ignored.
:param min_size: Discount all devices below this size in KB
:return: List containing drives of interest
"""
base_root_disk = root_disk()
cmd = [LSBLK, '-P', '-o',
'NAME,MODEL,SERIAL,SIZE,TRAN,VENDOR,HCTL,TYPE,FSTYPE,LABEL,UUID']
o, e, rc = run_command(cmd)
dnames = {} # Working dictionary of devices.
disks = [] # List derived from the final working dictionary of devices.
serials_seen = [] # List tally of serials seen during this scan.
# Stash variables to pass base info on root_disk to root device proper.
root_serial = root_model = root_transport = root_vendor = root_hctl = None
# To use udevadm to retrieve serial number rather than lsblk, make this True
# N.B. when lsblk returns no serial for a device then udev is used anyway.
always_use_udev_serial = False
device_names_seen = [] # List tally of devices seen during this scan
for line in o:
# skip processing of all lines that don't begin with "NAME"
if (re.match('NAME', line) is None):
continue
# setup our line / dev name dependant variables
# easy read categorization flags, all False until found otherwise.
is_root_disk = False # the base dev that / is mounted on ie system disk
is_partition = is_btrfs = False
dmap = {} # dictionary to hold line info from lsblk output eg NAME: sda
# line parser variables
cur_name = ''
cur_val = ''
name_iter = True
val_iter = False
sl = line.strip()
i = 0
while i < len(sl):
# We iterate over the line to parse it's information char by char
# keeping track of name or value and adding the char accordingly
if (name_iter and sl[i] == '=' and sl[i + 1] == '"'):
name_iter = False
val_iter = True
i = i + 2
elif (val_iter and sl[i] == '"' and
(i == (len(sl) - 1) or sl[i + 1] == ' ')):
val_iter = False
name_iter = True
i = i + 2
dmap[cur_name.strip()] = cur_val.strip()
cur_name = ''
cur_val = ''
elif (name_iter):
cur_name = cur_name + sl[i]
i = i + 1
elif (val_iter):
cur_val = cur_val + sl[i]
i = i + 1
else:
raise Exception('Failed to parse lsblk output: %s' % sl)
# md devices, such as mdadmin software raid and some hardware raid block
# devices show up in lsblk's output multiple times with identical info.
# Given we only need one copy of this info we remove duplicate device
# name entries, also offers more sane output to views/disk.py where name
# will be used as the index
if (dmap['NAME'] in device_names_seen):
continue
device_names_seen.append(dmap['NAME'])
# We are not interested in CD / DVD rom devices so skip to next device
if (dmap['TYPE'] == 'rom'):
continue
# We are not interested in swap partitions or devices so skip further
# processing and move to next device.
# N.B. this also facilitates a simpler mechanism of classification.
if (dmap['FSTYPE'] == 'swap'):
continue
# ----- Now we are done with easy exclusions we begin classification.
# ------------ Start more complex classification -------------
if (dmap['NAME'] == base_root_disk): # as returned by root_disk()
# We are looking at the system drive that hosts, either
# directly or as a partition, the / mount point.
# Given lsblk doesn't return serial, model, transport, vendor, hctl
# when displaying partitions we grab and stash them while we are
# looking at the root drive directly, rather than the / partition.
# N.B. assumption is lsblk first displays devices then partitions,
# this is the observed behaviour so far.
root_serial = dmap['SERIAL']
root_model = dmap['MODEL']
root_transport = dmap['TRAN']
root_vendor = dmap['VENDOR']
root_hctl = dmap['HCTL']
# Set readability flag as base_dev identified.
is_root_disk = True # root as returned by root_disk()
# And until we find a partition on this root disk we will label it
# as our root, this then allows for non partitioned root devices
# such as mdraid installs where root is directly on eg /dev/md126.
# N.B. this assumes base devs are listed before their partitions.
dmap['root'] = True
# Normal partitions are of type 'part', md partitions are of type 'md'
# normal disks are of type 'disk' md devices are of type eg 'raid1'.
# Disk members of eg intel bios raid md devices fstype='isw_raid_member'
# Note for future re-write; when using udevadm DEVTYPE, partition and
# disk works for both raid and non raid partitions and devices.
# Begin readability variables assignment
# - is this a partition regular or md type.
if (dmap['TYPE'] == 'part' or dmap['TYPE'] == 'md'):
is_partition = True
# - is filesystem of type btrfs
if (dmap['FSTYPE'] == 'btrfs'):
is_btrfs = True
# End readability variables assignment
if is_partition:
dmap['parted'] = True
# We don't account for partitions within partitions, but making
# an empty list here simplifies conditionals as always a list then.
dmap['partitions'] = []
# Search our working dictionary of already scanned devices by name
# We are assuming base devices are listed first and if of interest
# we have recorded it and can now back port it's partitioned status.
for dname in dnames.keys():
if (re.match(dname, dmap['NAME']) is not None):
# Our device name has a base device entry of interest saved:
# ie we have scanned and saved sdb but looking at sdb3 now.
# Given we have found a partition on an existing base dev
# we should update that base dev's entry in dnames to
# parted "True" as when recorded lsblk type on base device
# would have been disk or RAID1 or raid1 (for base md dev).
# Change the 12th entry (0 indexed) of this device to True
# The 12 entry is the parted flag so we label
# our existing base dev entry as parted ie partitioned.
dnames[dname][11] = True
# Also take this opportunity to back port software raid info
# from partitions to the base device if the base device
# doesn't already have an fstype identifying it's raid
# member status. For Example:-
# bios raid base dev gives lsblk FSTYPE="isw_raid_member";
# we already catch this directly.
# Pure software mdraid base dev has lsblk FSTYPE="" but a
# partition on this pure software mdraid that is a member
# of eg md125 has FSTYPE="linux_raid_member"
if dmap['FSTYPE'] == 'linux_raid_member' \
and (dnames[dname][8] is None):
# N.B. 9th item (index 8) in dname = FSTYPE
# We are a partition that is an mdraid raid member so
# backport this info to our base device ie sda1 raid
# member so label sda's FSTYPE entry the same as it's
# partition's entry if the above condition is met, ie
# only if the base device doesn't already have an
# FSTYPE entry ie None, this way we don't overwrite
# / loose info and we only need to have one partition
# identified as an mdraid member to classify the entire
# device (the base device) as a raid member, at least in
# part.
dnames[dname][8] = dmap['FSTYPE']
# Build a list of the partitions we find.
# Back port our current name as a partition entry in our
# base devices 'partitions' list 14th item (index 13).
dnames[dname][13].append(dmap['NAME'])
# This list is intended for use later in roles such as
# import / export devices or external backup drives so
# that the role config mechanism can offer up the known
# partitions found so that the eventual configured role
# will know which partition on the role based device to
# work with.
# Has one role per device limit but helps to keep usability
# and underlying disk management simpler.
else:
# We are not a partition so record this.
dmap['parted'] = False
# As we are not a partition it is assumed that we might hold a
# partition so start an empty partition list for this purpose.
# N.B. This assumes base devices are listed before their partitions.
dmap['partitions'] = []
# this list will be populated when we find our partitions and back
# port their names into this list.
if ((not is_root_disk and not is_partition) or
(is_btrfs)):
# We have a non system disk that is not a partition
# or
# We have a device that is btrfs formatted
# Or we may just be a non system disk without partitions.
dmap['root'] = False # until we establish otherwise as we might be.
if is_btrfs:
# a btrfs file system
if (re.match(base_root_disk, dmap['NAME']) is not None):
# We are assuming that a partition with a btrfs fs on is our
# root if it's name begins with our base system disk name.
# Now add the properties we stashed when looking at the base
# root disk rather than the root partition we see here.
dmap['SERIAL'] = root_serial
dmap['MODEL'] = root_model
dmap['TRAN'] = root_transport
dmap['VENDOR'] = root_vendor
dmap['HCTL'] = root_hctl
# As we have found root to be on a partition we can now
# un flag the base device as having been root prior to
# finding this partition on that base_root_disk
# N.B. Assumes base dev is listed before it's partitions
# The 13th item in dnames entries is root so index = 12.
# Only update our base_root_disk if it exists in our scanned
# disks as this may be the first time we are seeing it.
# Search to see if we already have an entry for the
# the base_root_disk which may be us or our base dev if we
# are a partition
for dname in dnames.keys():
if dname == base_root_disk:
dnames[base_root_disk][12] = False
# And update this device as real root
# Note we may be looking at the base_root_disk or one of
# it's partitions there after.
dmap['root'] = True
# TODO: The following clause on using model to hold member
# device into can be useful beyond the system disk.
# If we are an md device then use get_md_members string
# to populate our MODEL since it is otherwise unused.
if (re.match('md', dmap['NAME']) is not None):
# cheap way to display our member drives
dmap['MODEL'] = get_md_members(dmap['NAME'])
else:
# We have a non system disk btrfs filesystem.
# Ie we are a whole disk or a partition with btrfs on but
# NOT on the system disk.
# Most likely a current btrfs data drive or one we could
# import.
# As we don't understand / support btrfs in partitions
# then ignore / skip this btrfs device if it's a partition
if is_partition:
continue
# convert size into KB
size_str = dmap['SIZE']
if (size_str[-1] == 'G'):
dmap['SIZE'] = int(float(size_str[:-1]) * 1024 * 1024)
elif (size_str[-1] == 'T'):
dmap['SIZE'] = int(float(size_str[:-1]) * 1024 * 1024 * 1024)
else:
# Move to next line if we don't understand the size as GB or TB
# Note that this may cause an entry to be ignored if formatting
# changes.
# Previous to the explicit ignore swap clause this often caught
# swap but if swap was in GB and above min_size then it could
# show up when not in a partition (the previous caveat clause).
continue
if (dmap['SIZE'] < min_size):
continue
# No more continues so the device we have is to be passed to our db
# entry system views/disk.py ie _update_disk_state()
# Do final tidy of data in dmap and ready for entry in dnames dict.
# db needs unique serial so provide one where there is none found.
# First try harder with udev if lsblk failed on serial retrieval.
if (dmap['SERIAL'] == '' or always_use_udev_serial):
# lsblk fails to retrieve SERIAL from VirtIO drives and some
# sdcard devices and md devices so try specialized function.
dmap['SERIAL'] = get_disk_serial(dmap['NAME'], dmap['TYPE'])
if (dmap['SERIAL'] == '' or (dmap['SERIAL'] in serials_seen)):
# No serial number still or its a repeat.
# Overwrite drive serial entry in dmap with fake-serial- + uuid4
# See disk/disks_table.jst for a use of this flag mechanism.
# Previously we did dmap['SERIAL'] = dmap['NAME'] which is less
# robust as it can itself produce duplicate serial numbers.
dmap['SERIAL'] = 'fake-serial-' + str(uuid.uuid4())
# 12 chars (fake-serial-) + 36 chars (uuid4) = 48 chars
serials_seen.append(dmap['SERIAL'])
# replace all dmap values of '' with None.
for key in dmap.keys():
if (dmap[key] == ''):
dmap[key] = None
# transfer our device info as now parsed in dmap to the dnames dict
dnames[dmap['NAME']] = [dmap['NAME'], dmap['MODEL'],
dmap['SERIAL'], dmap['SIZE'],
dmap['TRAN'], dmap['VENDOR'],
dmap['HCTL'], dmap['TYPE'],
dmap['FSTYPE'], dmap['LABEL'],
dmap['UUID'], dmap['parted'],
dmap['root'], dmap['partitions'], ]
# Transfer our collected disk / dev entries of interest to the disks list.
for d in dnames.keys():
disks.append(Disk(*dnames[d]))
# logger.debug('ADDING disks LIST ELEMENT=%s' % Disk(*dnames[d]))
logger.debug('disks item = %s ', Disk(*dnames[d]))
logger.debug('scan_disks() returning %s' % disks)
return disks
def uptime():
with open('/proc/uptime') as ufo:
# TODO: check on readline here as reads a character at a time
# TODO: xreadlines() reads one line at a time.
return int(float(ufo.readline().split()[0]))
def def_kernel():
kernel = None
o, e, rc = run_command([GRUBBY, '--default-kernel'], throw=False)
if (len(o) > 0):
k_fields = o[0].split('/boot/vmlinuz-')
if (len(k_fields) == 2):
kernel = k_fields[1]
return kernel
def kernel_info(supported_version):
uname = os.uname()
if (uname[2] != supported_version):
e_msg = ('You are running an unsupported kernel(%s). Some features '
'may not work properly.' % uname[2])
run_command([GRUBBY, '--set-default=/boot/vmlinuz-%s' % supported_version])
e_msg = ('%s Please reboot and the system will '
'automatically boot using the supported kernel(%s)' %
(e_msg, supported_version))
raise Exception(e_msg)
return uname[2]
def create_tmp_dir(dirname):
# TODO: suggest name change to create_dir
return run_command([MKDIR, '-p', dirname])
def rm_tmp_dir(dirname):
# TODO: suggest name change to remove_dir
return run_command([RMDIR, dirname])
def nfs4_mount_teardown(export_pt):
"""
reverse of setup. cleanup when there are no more exports
"""
if (is_mounted(export_pt)):
run_command([UMOUNT, '-l', export_pt])
for i in range(10):
if (not is_mounted(export_pt)):
return run_command([RMDIR, export_pt])
time.sleep(1)
run_command([UMOUNT, '-f', export_pt])
if (os.path.exists(export_pt)):
return run_command([RMDIR, export_pt])
return True
def bind_mount(mnt_pt, export_pt):
if (not is_mounted(export_pt)):
run_command([MKDIR, '-p', export_pt])
return run_command([MOUNT, '--bind', mnt_pt, export_pt])
return True
def refresh_nfs_exports(exports):
"""
input format:
{'export_point': [{'client_str': 'www.example.com',
'option_list': 'rw,insecure,'
'mnt_pt': mnt_pt,},],
...}
if 'clients' is an empty list, then unmount and cleanup.
"""
fo, npath = mkstemp()
with open(npath, 'w') as efo:
shares = []
for e in exports.keys():
if (len(exports[e]) == 0):
# do share tear down at the end, only snaps here
if (len(e.split('/')) == 4):
nfs4_mount_teardown(e)
else:
shares.append(e)
continue
if (not is_mounted(e)):
bind_mount(exports[e][0]['mnt_pt'], e)
client_str = ''
admin_host = None
for c in exports[e]:
run_command([EXPORTFS, '-i', '-o', c['option_list'],
'%s:%s' % (c['client_str'], e)])
client_str = ('%s%s(%s) ' % (client_str, c['client_str'],
c['option_list']))
if ('admin_host' in c):
admin_host = c['admin_host']
if (admin_host is not None):
run_command([EXPORTFS, '-i', '-o', 'rw,no_root_squash',
'%s:%s' % (admin_host, e)])
client_str = ('%s %s(rw,no_root_squash)' % (client_str,
admin_host))
export_str = ('%s %s\n' % (e, client_str))
efo.write(export_str)
for s in shares:
nfs4_mount_teardown(s)
shutil.move(npath, '/etc/exports')
return run_command([EXPORTFS, '-ra'])
def config_network_device(name, dtype='ethernet', method='auto', ipaddr=None,
netmask=None, autoconnect='yes', gateway=None,
dns_servers=None):
#1. delete any existing connections that are using the given device.
show_cmd = [NMCLI, 'c', 'show']
o, e, rc = run_command(show_cmd)
for l in o:
fields = l.strip().split()
if (len(fields) > 3 and fields[-1] == name):
#fields[-3] is the uuid of the connection
run_command([NMCLI, 'c', 'delete', fields[-3]])
#2. Add a new connection
add_cmd = [NMCLI, 'c', 'add', 'type', dtype, 'con-name', name, 'ifname', name]
if (method == 'manual'):
add_cmd.extend(['ip4', '%s/%s' % (ipaddr, netmask)])
if (gateway is not None and len(gateway.strip()) > 0):
add_cmd.extend(['gw4', gateway])
run_command(add_cmd)
#3. modify with extra options like dns servers
if (method == 'manual'):
mod_cmd = [NMCLI, 'c', 'mod', name, ]
if (dns_servers is not None and len(dns_servers.strip()) > 0):
mod_cmd.extend(['ipv4.dns', dns_servers])
if (autoconnect == 'no'):
mod_cmd.extend(['connection.autoconnect', 'no'])
if (len(mod_cmd) > 4):
run_command(mod_cmd)
run_command([NMCLI, 'c', 'up', name])
#wait for the interface to be activated
num_attempts = 0
while True:
state = get_net_config(name)[name].get('state', None)
if (state != 'activated'):
time.sleep(1)
num_attempts += 1
else:
break
if (num_attempts > 30):
msg = ('Waited %s seconds for connection(%s) state to '
'be activated but it has not. Giving up. current state: %s'
% (num_attempts, name, state))
raise Exception(msg)
def convert_netmask(bits):
#convert netmask bits into ip representation
bits = int(bits)
mask = 0
for i in xrange(32-bits,32):
mask |= (1 << i)
return inet_ntoa(pack('>I', mask))
def net_config_helper(name):
config = {}
o, e, rc = run_command([NMCLI, '-t', 'c', 'show', name], throw=False)
if (rc == 10):
return config
for l in o:
l = l.strip()
if ('method' in config):
if (config['method'] == 'auto'):
#dhcp
if (re.match('DHCP4.OPTION.*ip_address = .+', l) is not None):
config['ipaddr'] = l.split('= ')[1]
elif (re.match('DHCP4.OPTION.*:domain_name_servers = .+', l) is not None):
config['dns_servers'] = l.split('= ')[1]
elif (re.match('DHCP4.OPTION.*:subnet_mask = .+', l) is not None):
config['netmask'] = l.split('= ')[1]
elif (re.match('IP4.GATEWAY:.+', l) is not None):
config['gateway'] = l.split(':')[1]
elif (config['method'] == 'manual'):
#manual
if (re.match('IP4.ADDRESS', l) is not None):
kv_split = l.split(':')
if (len(kv_split) > 1):
vsplit = kv_split[1].split('/')
if (len(vsplit) > 0):
config['ipaddr'] = vsplit[0]
if (len(vsplit) > 1):
config['netmask'] = convert_netmask(vsplit[1])
elif (re.match('ipv4.dns:.+', l) is not None):
config['dns_servers'] = l.split(':')[1]
elif (re.match('ipv4.gateway:.+', l) is not None):
config['gateway'] = l.split(':')[1]
else:
raise Exception('Unknown ipv4.method(%s). ' % config['method'])
if (re.match('connection.interface-name:', l) is not None):
config['name'] = l.split(':')[1]
elif (re.match('connection.autoconnect:', l) is not None):
config['autoconnect'] = l.split(':')[1]
elif (re.match('ipv4.method:.+', l) is not None):
config['method'] = l.split(':')[1]
if (re.match('GENERAL.DEVICES:.+', l) is not None):
config['dname'] = l.split(':')[1]
elif (re.match('connection.type:.+', l) is not None):
config['ctype'] = l.split(':')[1]
elif (re.match('GENERAL.STATE:.+', l) is not None):
config['state'] = l.split(':')[1]
if ('dname' in config):
o, e, rc = run_command([NMCLI, '-t', '-f', 'all', 'd', 'show', config['dname'],])
for l in o:
l = l.strip()
if (re.match('GENERAL.TYPE:.+', l) is not None):
config['dtype'] = l.split(':')[1]
elif (re.match('GENERAL.HWADDR:.+', l) is not None):
config['mac'] = l.split('GENERAL.HWADDR:')[1]
elif (re.match('CAPABILITIES.SPEED:.+', l) is not None):
config['dspeed'] = l.split(':')[1]
return config
def get_net_config(all=False, name=None):
if (all):
o, e, rc = run_command([NMCLI, '-t', 'd', 'show'])
devices = []
config = {}
for i in range(len(o)):
if (re.match('GENERAL.DEVICE:', o[i]) is not None and
re.match('GENERAL.TYPE:', o[i+1]) is not None and
o[i+1].strip().split(':')[1] == 'ethernet'):
dname = o[i].strip().split(':')[1]
config[dname] = {'dname': dname,
'mac': o[i+2].strip().split('GENERAL.HWADDR:')[1],
'name': o[i+5].strip().split('GENERAL.CONNECTION:')[1],
'dtype': 'ethernet', }
for device in config:
config[device].update(net_config_helper(config[device]['name']))
if (config[device]['name'] == '--'):
config[device]['name'] = device
return config
return {name: net_config_helper(name),}
def update_issue(ipaddr):
msg = ("\n\nYou can go to RockStor's webui by pointing your web browser"
" to https://%s\n\n" % ipaddr)
with open('/etc/issue', 'w') as ifo:
ifo.write(msg)
def sethostname(hostname):
return run_command([HOSTNAMECTL, 'set-hostname', hostname])
def gethostname():
o, e, rc = run_command([HOSTNAMECTL, '--static'])
return o[0]
def is_share_mounted(sname, mnt_prefix=DEFAULT_MNT_DIR):
mnt_pt = mnt_prefix + sname
return is_mounted(mnt_pt)
def is_mounted(mnt_pt):
with open('/proc/mounts') as pfo:
for line in pfo.readlines():
if (re.search(' ' + mnt_pt + ' ', line) is not None):
return True
return False
def remount(mnt_pt, mnt_options):
if (is_mounted(mnt_pt)):
run_command([MOUNT, '-o', 'remount,%s' % mnt_options, mnt_pt])
return True
def wipe_disk(disk_byid):
"""
Simple run_command wrapper to execute "wipefs -a disk_byid"
:param disk_byid: by-id type name without path as found in db Disks.name.
:return: o, e, rc tuple returned by the run_command wrapper running the
locally generated wipefs command.
"""
disk_byid_withpath = ('/dev/disk/by-id/%s' % disk_byid)
return run_command([WIPEFS, '-a', disk_byid_withpath])
def blink_disk(disk_byid, total_exec, read, sleep):
"""
Method to cause a drives activity light to blink by parameter defined
timings to aid in physically locating an attached disk.
Works by causing drive activity via a dd read to null from the disk_byid.
N.B. Utilises subprocess and signal to run on dd on an independent thread.
:param disk_byid: by-id type disk name without path
:param total_exec: Total time to blink the drive light.
:param read: Read (light on) time.
:param sleep: light off time.
:return: None.
"""
dd_cmd = [DD, 'if=/dev/disk/by-id/%s' % disk_byid, 'of=/dev/null', 'bs=512',
'conv=noerror']
p = subprocess.Popen(dd_cmd, shell=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
total_elapsed_time = 0
while (total_elapsed_time < total_exec):
if (p.poll() is not None):
return None
time.sleep(read)
p.send_signal(signal.SIGSTOP)
time.sleep(sleep)
total_elapsed_time += read + sleep
p.send_signal(signal.SIGCONT)
p.terminate()
def convert_to_kib(size):
"""
Attempts to convert a size string that is initially expected to have 3
letters indicating it's units, into the equivalent KiB units.
If no known 3 letter Unit string is identified and 'B' is found then 0 is
returned.
Upon no string match is found in the above process for 'size' unit then an
exception is raised to this effect.
:param size: String parameter to process
:return: post processed size parameter expressed in equivalent integer KiB's
or zero if units in size are found to 'B'
"""
suffix_map = {
'KiB': 1,
'MiB': 1024,
'GiB': 1024 * 1024,
'TiB': 1024 * 1024 * 1024,
'PiB': 1024 * 1024 * 1024 * 1024, }
suffix = size[-3:]
num = size[:-3]
if (suffix not in suffix_map):
if (size[-1] == 'B'):
return 0
raise Exception('Unknown suffix(%s) while converting to KiB' % suffix)
return int(float(num) * suffix_map[suffix])
def root_disk():
"""
Returns the base drive device name where / mount point is found.
Works by parsing /proc/mounts. Eg if the root entry was as follows:
/dev/sdc3 / btrfs rw,noatime,ssd,space_cache,subvolid=258,subvol=/root 0 0
the returned value is sdc
The assumption with non md devices is that the partition number will be a
single character.
:return: sdX type device name (without path) where root is mounted.
"""
with open('/proc/mounts') as fo:
for line in fo.readlines():
fields = line.split()
if (fields[1] == '/' and fields[2] == 'btrfs'):
disk = os.path.realpath(fields[0])
if (re.match('/dev/md', disk) is not None):
# We have an Multi Device naming scheme which is a little
# different ie 3rd partition = md126p3 on the md126 device,
# or md0p3 as third partition on md0 device.
# As md devs often have 1 to 3 numerical chars we search
# for one or more numeric characters, this assumes our dev
# name has no prior numerical components ie starts /dev/md
# but then we are here due to that match.
# Find the indexes of the device name without the partition.
# Search for where the numbers after "md" end.
# N.B. the following will also work if root is not in a
# partition ie on md126 directly.
end = re.search('\d+', disk).end()
return disk[5:end]
if (re.match('/dev/nvme', disk) is not None):
# We have an nvme device. These have the following naming
# conventions.
# Base device examples: nvme0n1 or nvme1n1
# First partition on the first device would be nvme0n1p1
# The first number after 'nvme' is the device number.
# Partitions are indicated by the p# combination ie 'p1'.
# We need to also account for a root install on the base
# device itself as with the /dev/md parsing just in case,
# so look for the end of the base device name via 'n1'.
end = re.search('n1', disk).end()
return disk[5:end]
# catch all that assumes we have eg /dev/sda3 and want "sda"
# so start from 6th char and remove the last char
# /dev/sda3 = sda
# TODO: consider changing to same method as in md devs above
# TODO: to cope with more than one numeric in name.
return disk[5:-1]
msg = ('root filesystem is not BTRFS. During Rockstor installation, '
'you must select BTRFS instead of LVM and other options for '
'root filesystem. Please re-install Rockstor properly.')
raise NonBTRFSRootException(msg)
def get_md_members(device_name, test=None):
"""
Returns the md members from a given device, if the given device is not an
md device or the udevadm info command returns a non 0 (error) then the an
empty string is returned.
Example lines to parse from udevadmin:-
E: MD_DEVICE_sda_DEV=/dev/sda
E: MD_DEVICE_sda_ROLE=0
E: MD_DEVICE_sdb_DEV=/dev/sdb
E: MD_DEVICE_sdb_ROLE=1
Based on the get_disk_serial function.
N.B. may be deprecated on scan_disks move to udevadmin, or integrated.
Could consider parsing "mdadm --detail /dev/md1" instead
:param device_name: eg md126 or md0p2
:param test: if test is not None then it's contents is used in lieu of
udevadm output.
:return: String of all members listed in udevadm info --name=device_name
example: "[2]-/dev/sda[0]-/dev/sdb[1]-raid1" = 2 devices with level info
"""
line_fields = []
# if non md device then return empty string
if re.match('md', device_name) is None:
return ''
members_string = ''
if test is None:
out, err, rc = run_command([UDEVADM, 'info', '--name=' + device_name],
throw=False)
else:
# test mode so process test instead of udevadmin output
out = test
rc = 0
if rc != 0: # if return code is an error return empty string
return ''
# search output of udevadmin to find all current md members.
for line in out:
if line == '':
continue
# nonlocal line_fields
line_fields = line.strip().replace('=', ' ').split()
# fast replace of '=' with space so split() can divide all fields
# example original line "E: ID_SERIAL_SHORT=S1D5NSAF111111K"
# less than 3 fields are of no use so just in case:-
if len(line_fields) < 3:
continue
# catch the lines that begin with MD_DEVICE or MD_LEVEL
if (re.match('MD_DEVICE|MD_LEVEL', line_fields[1]) is not None):
# add this entries value (3rd column) to our string
if len(line_fields[2]) == 1:
# Surround single digits with square brackets ie the number of
# members and the member index (assumes max 9 md members)
members_string += '[' + line_fields[2] + '] '
else:
if re.match('/dev', line_fields[2]) is not None:
# TODO: get_disk_serial can benefit from a device type
# TODO: consider calling lsblk -n -o 'TYPE' device_name
# TODO: may then allow for /dev/mapper raid members.
# We have a dev name so put it's serial in our string.
members_string += get_disk_serial(line_fields[2])
else:
# > 1 char value that doesn't start with /dev, so raid level
members_string += line_fields[2]
return members_string
def get_disk_serial(device_name, device_type=None, test=None):
"""
Returns the serial number of device_name using udevadm to match that
returned by lsblk. N.B. udevadm has been observed to return the following:-
ID_SCSI_SERIAL rarely seen
ID_SERIAL_SHORT often seen
ID_SERIAL thought to always be seen (see note below)
N.B. if used in this order the serial is most likely to resemble that shown
on the device label as well as that returned by the lsblk. ID_SERIAL seems
always to appear but is sometimes accompanied by one or both of the others.
When ID_SERIAL is accompanied by ID_SERIAL_SHORT the short variant is
closer to lsblk and physical label. When they are both present the
ID_SERIAL appears to be a combination of the model and the ID_SERIAL_SHORT
---------
Additional personality added for md devices ie md0p1 or md126, these devices
have no serial so we search for their MD_UUID and use that instead.
:param device_name: eg sda as per lsblk output used in scan_disks()
:param device_type: the lsblk TYPE for the given device eg: disk, crypt.
The equivalent to the output of lsblk -n -o TYPE device_name. Defaults to
None as an indication that the caller cannot provide this info.
:param test: When not None this parameter's contents is substituted for the
return of the udevadm info --name=device_name command output
:return: 12345678901234567890 or empty string if no serial was retrieved.
"""
serial_num = ''
uuid_search_string = ''
line_fields = []
# udevadm requires the full path for Device Mapped (DM) disks so if our
# type indicates this then add the '/dev/mapper' path to device_name
# Set search string / flag for dm personality if need be.
if device_type == 'crypt':
device_name = '/dev/mapper/%s' % device_name
# Assuming device mapped (DM) so without it's own serial.
uuid_search_string = 'DM_UUID'
# TODO: Could just get uuid via "cryptsetup luksUUID <device>" as
# TODO: then the serial is stable between mapper name changes so tracks
# TODO: the underlying container.
# Set search string / flag for md personality if need be.
if re.match('md', device_name) is not None:
uuid_search_string = 'MD_UUID'
if test is None:
out, err, rc = run_command([UDEVADM, 'info', '--name=' + device_name],
throw=False)
else:
# test mode so process test instead of udevadmin output
out = test
rc = 0
if rc != 0: # if return code is an error return empty string
return ''
for line in out:
if line == '':
continue
# nonlocal line_fields
line_fields = line.strip().replace('=', ' ').split()
# fast replace of '=' with space so split() can divide all fields
# example original line "E: ID_SERIAL_SHORT=S1D5NSAF111111K"
# less than 3 fields are of no use so just in case:-
if len(line_fields) < 3:
continue
# For md & dm devices, look for MD_UUID or DM_UUID respectively and use
# as substitute for no hw serial.
if uuid_search_string != '':
# md or dm device so search for the appropriate uuid string
if line_fields[1] == uuid_search_string:
# TODO: in the case of DM_UUID consider extracting only the
# TODO: UUID to cope with container mount point changes
serial_num = line_fields[2]
# we have found our hw serial equivalent so break to return
break
else: # we are md / dm device but haven't found our UUID line
# move to next line of output and skip serial cascade search
continue
if line_fields[1] == 'ID_SCSI_SERIAL':
# we have an instance of SCSI_SERIAL being more reliably unique
# when present than SERIAL_SHORT or SERIAL so overwrite whatever
# we have and look no further by breaking out of the search loop
serial_num = line_fields[2]
break
elif line_fields[1] == 'ID_SERIAL_SHORT':
# SERIAL_SHORT is better than SERIAL so just overwrite whatever we
# have so far with SERIAL_SHORT
serial_num = line_fields[2]
else:
if line_fields[1] == 'ID_SERIAL':
# SERIAL is sometimes our only option but only use it if we
# have found nothing else.
if serial_num == '':
serial_num = line_fields[2]
# should return one of the following in order of priority
# SCSI_SERIAL, SERIAL_SHORT, SERIAL
return serial_num
def get_virtio_disk_serial(device_name):
"""
N.B. this function is deprecated by get_disk_serial
Returns the serial number of device_name virtio disk eg /dev/vda
Returns empty string if cat /sys/block/vda/serial command fails
Note no serial entry in /sys/block/sda/ for real or KVM sata drives
:param device_name: eg vda
:return: 12345678901234567890
Note maximum length of serial number reported = 20 chars
But longer serial numbers can be specified in the VM XML spec file.
The virtio block device is itself limited to 20 chars ie:-
https://github.com/qemu/qemu/blob/
a9392bc93c8615ad1983047e9f91ee3fa8aae75f/include/standard-headers/
linux/virtio_blk.h
#define VIRTIO_BLK_ID_BYTES 20 /* ID string length */
This process may not deal well with spaces in the serial number
but VMM does not allow this.
"""
dev_path = ('/sys/block/%s/serial' % device_name)
out, err, rc = run_command([CAT, dev_path], throw=False)
if (rc != 0):
return ''
# our out list has one element that is the serial number, like ['11111111111111111111']
return out[0]
def system_shutdown():
return run_command([SHUTDOWN, '-h', 'now'])
def system_reboot():
return run_command([SHUTDOWN, '-r', 'now'])
def md5sum(fpath):
# return the md5sum of the given file
if (not os.path.isfile(fpath)):
return None
md5 = hashlib.md5()
with open(fpath) as tfo:
for l in tfo.readlines():