Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
N
nmvetcli-mirror
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
JetBrains YouTrack
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
KSZK
Sysadmin
Archived
nmvetcli-mirror
Commits
86b1bca7
Commit
86b1bca7
authored
9 years ago
by
Christoph Hellwig
Browse files
Options
Downloads
Patches
Plain Diff
nvmet,nvmetcli: add support for host NQN based access control
Signed-off-by:
Christoph Hellwig
<
hch@lst.de
>
parent
fbf48aed
No related branches found
No related tags found
No related merge requests found
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
README.md
+22
-8
22 additions, 8 deletions
README.md
nvmet/nvme.py
+105
-0
105 additions, 0 deletions
nvmet/nvme.py
nvmet/test_nvmet.py
+73
-0
73 additions, 0 deletions
nvmet/test_nvmet.py
nvmetcli
+101
-0
101 additions, 0 deletions
nvmetcli
with
301 additions
and
8 deletions
README.md
+
22
−
8
View file @
86b1bca7
...
...
@@ -38,24 +38,38 @@ arguments. Then in the nvmetcli prompt type:
#
> cd /subsystems
/subsystems> create testnqn
...> create testnqn
#
# Add access for a specific NVMe Host by it's NQN:
#
...> cd /hosts
...> create hostnqn
...> cd /subsystems/testnqn/allowed_hosts/
...> create hostnqn
#
# Alternatively this allows any host to connect to the subsystsem. Only
# use this in tightly controller environments:
#
...> cd /subsystems/testnqn/
...> set attr allow_any_host=1
#
# Create a new namespace. If you do not specify a namespace ID the fist
# unused one will be used.
#
/subsystems> cd testnqn/
/subsystems/testnqn> cd namespaces
/subsystems/testnqn/namespaces> create 1
/subsystems/testnqn/namespaces> cd 1
/subsystems/t.../namespaces/1> set device path=/dev/ram1
/subsystems/t.../namespaces/1> enable
...> cd namespaces
...> create 1
...> cd 1
...> set device path=/dev/ram1
...> enable
Testing
-------
nvmetcli comes with a testsuite that tests itsel
s
and the kernel configfs
nvmetcli comes with a testsuite that tests itsel
f
and the kernel configfs
interface for the NVMe target. To run it make sure you have nose2 and
the coverage plugin for it installed and simple run 'make test'.
This diff is collapsed.
Click to expand it.
nvmet/nvme.py
+
105
−
0
View file @
86b1bca7
...
...
@@ -272,6 +272,15 @@ class Root(CFSNode):
ports
=
property
(
_list_ports
,
doc
=
"
Get the list of Ports.
"
)
def
_list_hosts
(
self
):
self
.
_check_self
()
for
h
in
os
.
listdir
(
"
%s/hosts/
"
%
self
.
_path
):
yield
Host
(
h
,
'
lookup
'
)
hosts
=
property
(
_list_hosts
,
doc
=
"
Get the list of Hosts.
"
)
def
save_to_file
(
self
,
savefile
=
None
):
'''
Write the configuration in json format to a file.
...
...
@@ -300,6 +309,8 @@ class Root(CFSNode):
s
.
delete
()
for
p
in
self
.
ports
:
p
.
delete
()
for
h
in
self
.
hosts
:
h
.
delete
()
def
restore
(
self
,
config
,
clear_existing
=
False
,
abort_on_error
=
False
):
'''
...
...
@@ -323,6 +334,14 @@ class Root(CFSNode):
def
err_func
(
err_str
):
errors
.
append
(
err_str
+
"
, skipped
"
)
# Create the hosts first because the subsystems reference them
for
index
,
t
in
enumerate
(
config
.
get
(
'
hosts
'
,
[])):
if
'
nqn
'
not
in
t
:
err_func
(
"'
nqn
'
not defined in host %d
"
%
index
)
continue
Host
.
setup
(
t
,
err_func
)
for
index
,
t
in
enumerate
(
config
.
get
(
'
subsystems
'
,
[])):
if
'
nqn
'
not
in
t
:
err_func
(
"'
nqn
'
not defined in subsystem %d
"
%
index
)
...
...
@@ -360,6 +379,7 @@ class Root(CFSNode):
d
=
super
(
Root
,
self
).
dump
()
d
[
'
subsystems
'
]
=
[
s
.
dump
()
for
s
in
self
.
subsystems
]
d
[
'
ports
'
]
=
[
p
.
dump
()
for
p
in
self
.
ports
]
d
[
'
hosts
'
]
=
[
h
.
dump
()
for
h
in
self
.
hosts
]
return
d
...
...
@@ -393,6 +413,7 @@ class Subsystem(CFSNode):
nqn
=
self
.
_generate_nqn
()
self
.
nqn
=
nqn
self
.
attr_groups
=
[
'
attr
'
]
self
.
_path
=
"
%s/subsystems/%s
"
%
(
self
.
configfs_dir
,
nqn
)
self
.
_create_in_cfs
(
mode
)
...
...
@@ -415,11 +436,39 @@ class Subsystem(CFSNode):
self
.
_check_self
()
for
ns
in
self
.
namespaces
:
ns
.
delete
()
for
h
in
self
.
allowed_hosts
:
self
.
remove_allowed_host
(
h
)
super
(
Subsystem
,
self
).
delete
()
namespaces
=
property
(
_list_namespaces
,
doc
=
"
Get the list of Namespaces for the Subsystem.
"
)
def
_list_allowed_hosts
(
self
):
return
[
os
.
path
.
basename
(
name
)
for
name
in
os
.
listdir
(
"
%s/allowed_hosts/
"
%
self
.
_path
)]
allowed_hosts
=
property
(
_list_allowed_hosts
,
doc
=
"
Get the list of Allowed Hosts for the Subsystem.
"
)
def
add_allowed_host
(
self
,
nqn
):
'''
Enable access for the host identified by I{nqn} to the Subsystem
'''
try
:
os
.
symlink
(
"
%s/hosts/%s
"
%
(
self
.
configfs_dir
,
nqn
),
"
%s/allowed_hosts/%s
"
%
(
self
.
_path
,
nqn
))
except
Exception
as
e
:
raise
CFSError
(
"
Could not symlink %s in configFS: %s
"
%
(
nqn
,
e
))
def
remove_allowed_host
(
self
,
nqn
):
'''
Disable access for the host identified by I{nqn} to the Subsystem
'''
try
:
os
.
unlink
(
"
%s/allowed_hosts/%s
"
%
(
self
.
_path
,
nqn
))
except
Exception
as
e
:
raise
CFSError
(
"
Could not unlink %s in configFS: %s
"
%
(
nqn
,
e
))
@classmethod
def
setup
(
cls
,
t
,
err_func
):
'''
...
...
@@ -440,11 +489,16 @@ class Subsystem(CFSNode):
for
ns
in
t
.
get
(
'
namespaces
'
,
[]):
Namespace
.
setup
(
s
,
ns
,
err_func
)
for
h
in
t
.
get
(
'
allowed_hosts
'
,
[]):
s
.
add_allowed_host
(
h
)
s
.
_setup_attrs
(
t
,
err_func
)
def
dump
(
self
):
d
=
super
(
Subsystem
,
self
).
dump
()
d
[
'
nqn
'
]
=
self
.
nqn
d
[
'
namespaces
'
]
=
[
ns
.
dump
()
for
ns
in
self
.
namespaces
]
d
[
'
allowed_hosts
'
]
=
self
.
allowed_hosts
return
d
...
...
@@ -600,6 +654,57 @@ class Port(CFSNode):
return
d
class
Host
(
CFSNode
):
'''
This is an interface to a NVMe Host in configFS.
A Host is identified by its NQN.
'''
def
__repr__
(
self
):
return
"
<Host %s>
"
%
self
.
nqn
def
__init__
(
self
,
nqn
,
mode
=
'
any
'
):
'''
@param nqn: The Hosts
'
s NQN.
@type nqn: string
@param mode:An optional string containing the object creation mode:
- I{
'
any
'
} means the configFS object will be either looked up
or created.
- I{
'
lookup
'
} means the object MUST already exist configFS.
- I{
'
create
'
} means the object must NOT already exist in configFS.
@type mode:string
@return: A Host object.
'''
super
(
Host
,
self
).
__init__
()
self
.
nqn
=
nqn
self
.
_path
=
"
%s/hosts/%s
"
%
(
self
.
configfs_dir
,
nqn
)
self
.
_create_in_cfs
(
mode
)
@classmethod
def
setup
(
cls
,
t
,
err_func
):
'''
Set up Host objects based upon t dict, from saved config.
Guard against missing or bad dict items, but keep going.
Call
'
err_func
'
for each error.
'''
if
'
nqn
'
not
in
t
:
err_func
(
"'
nqn
'
not defined for Host
"
)
return
try
:
h
=
Host
(
t
[
'
nqn
'
])
except
CFSError
as
e
:
err_func
(
"
Could not create Host object: %s
"
%
e
)
return
def
dump
(
self
):
d
=
super
(
Host
,
self
).
dump
()
d
[
'
nqn
'
]
=
self
.
nqn
return
d
def
_test
():
from
doctest
import
testmod
testmod
()
...
...
This diff is collapsed.
Click to expand it.
nvmet/test_nvmet.py
+
73
−
0
View file @
86b1bca7
...
...
@@ -245,6 +245,67 @@ class TestNvmet(unittest.TestCase):
p
.
set_enable
(
1
)
p
.
delete
()
def
test_host
(
self
):
root
=
nvme
.
Root
()
root
.
clear_existing
()
for
p
in
root
.
hosts
:
self
.
assertTrue
(
False
,
'
Found Host after clear
'
)
# create mode
h1
=
nvme
.
Host
(
nqn
=
'
foo
'
,
mode
=
'
create
'
)
self
.
assertIsNotNone
(
h1
)
self
.
assertEqual
(
len
(
list
(
root
.
hosts
)),
1
)
# any mode, should create
h2
=
nvme
.
Host
(
nqn
=
'
bar
'
,
mode
=
'
any
'
)
self
.
assertIsNotNone
(
h2
)
self
.
assertEqual
(
len
(
list
(
root
.
hosts
)),
2
)
# duplicate
self
.
assertRaises
(
nvme
.
CFSError
,
nvme
.
Host
,
'
foo
'
,
mode
=
'
create
'
)
self
.
assertEqual
(
len
(
list
(
root
.
hosts
)),
2
)
# lookup using any, should not create
h
=
nvme
.
Host
(
'
foo
'
,
mode
=
'
any
'
)
self
.
assertEqual
(
h1
,
h
)
self
.
assertEqual
(
len
(
list
(
root
.
hosts
)),
2
)
# lookup only
h
=
nvme
.
Host
(
'
bar
'
,
mode
=
'
lookup
'
)
self
.
assertEqual
(
h2
,
h
)
self
.
assertEqual
(
len
(
list
(
root
.
hosts
)),
2
)
# and delete them all
for
h
in
root
.
hosts
:
h
.
delete
()
self
.
assertEqual
(
len
(
list
(
root
.
hosts
)),
0
)
def
test_allowed_hosts
(
self
):
root
=
nvme
.
Root
()
h
=
nvme
.
Host
(
nqn
=
'
hostnqn
'
,
mode
=
'
create
'
)
s
=
nvme
.
Subsystem
(
nqn
=
'
testnqn
'
,
mode
=
'
create
'
)
# add allowed_host
s
.
add_allowed_host
(
nqn
=
'
hostnqn
'
)
# duplicate
self
.
assertRaises
(
nvme
.
CFSError
,
s
.
add_allowed_host
,
'
hostnqn
'
)
# invalid
self
.
assertRaises
(
nvme
.
CFSError
,
s
.
add_allowed_host
,
'
invalid
'
)
# remove again
s
.
remove_allowed_host
(
'
hostnqn
'
)
# duplicate removal
self
.
assertRaises
(
nvme
.
CFSError
,
s
.
remove_allowed_host
,
'
hostnqn
'
)
# invalid removal
self
.
assertRaises
(
nvme
.
CFSError
,
s
.
remove_allowed_host
,
'
foobar
'
)
def
test_invalid_input
(
self
):
root
=
nvme
.
Root
()
root
.
clear_existing
()
...
...
@@ -271,7 +332,13 @@ class TestNvmet(unittest.TestCase):
root
=
nvme
.
Root
()
root
.
clear_existing
()
h
=
nvme
.
Host
(
nqn
=
'
hostnqn
'
,
mode
=
'
create
'
)
s
=
nvme
.
Subsystem
(
nqn
=
'
testnqn
'
,
mode
=
'
create
'
)
s
.
add_allowed_host
(
nqn
=
'
hostnqn
'
)
s2
=
nvme
.
Subsystem
(
nqn
=
'
testnqn2
'
,
mode
=
'
create
'
)
s2
.
set_attr
(
'
attr
'
,
'
allow_any_host
'
,
1
)
n
=
nvme
.
Namespace
(
s
,
nsid
=
42
,
mode
=
'
create
'
)
n
.
set_attr
(
'
device
'
,
'
path
'
,
'
/dev/ram0
'
)
...
...
@@ -300,10 +367,16 @@ class TestNvmet(unittest.TestCase):
root
.
restore_from_file
(
'
test.json
'
,
True
)
# rebuild our view of the world
h
=
nvme
.
Host
(
nqn
=
'
hostnqn
'
,
mode
=
'
lookup
'
)
s
=
nvme
.
Subsystem
(
nqn
=
'
testnqn
'
,
mode
=
'
lookup
'
)
s2
=
nvme
.
Subsystem
(
nqn
=
'
testnqn2
'
,
mode
=
'
lookup
'
)
n
=
nvme
.
Namespace
(
s
,
nsid
=
42
,
mode
=
'
lookup
'
)
p
=
nvme
.
Port
(
root
,
portid
=
66
,
mode
=
'
lookup
'
)
self
.
assertEqual
(
s
.
get_attr
(
'
attr
'
,
'
allow_any_host
'
),
"
0
"
)
self
.
assertEqual
(
s2
.
get_attr
(
'
attr
'
,
'
allow_any_host
'
),
"
1
"
)
self
.
assertIn
(
'
hostnqn
'
,
s
.
allowed_hosts
)
# and check everything is still the same
self
.
assertTrue
(
n
.
get_enable
())
self
.
assertEqual
(
n
.
get_attr
(
'
device
'
,
'
path
'
),
'
/dev/ram0
'
)
...
...
This diff is collapsed.
Click to expand it.
nvmetcli
+
101
−
0
View file @
86b1bca7
...
...
@@ -95,6 +95,7 @@ class UIRootNode(UINode):
self
.
_children
=
set
([])
UISubsystemsNode
(
self
)
UIPortsNode
(
self
)
UIHostsNode
(
self
)
def
ui_command_restoreconfig
(
self
,
savefile
=
None
,
clear_existing
=
False
):
'''
...
...
@@ -151,6 +152,7 @@ class UISubsystemNode(UINode):
def
refresh
(
self
):
self
.
_children
=
set
([])
UINamespacesNode
(
self
)
UIAllowedHostsNode
(
self
)
class
UINamespacesNode
(
UINode
):
...
...
@@ -239,6 +241,66 @@ class UINamespaceNode(UINode):
"
The Namespace could not be disabled.
"
)
class
UIAllowedHostsNode
(
UINode
):
def
__init__
(
self
,
parent
):
UINode
.
__init__
(
self
,
'
allowed_hosts
'
,
parent
)
def
refresh
(
self
):
self
.
_children
=
set
([])
for
host
in
self
.
parent
.
cfnode
.
allowed_hosts
:
UIAllowedHostNode
(
self
,
host
)
def
ui_command_create
(
self
,
nqn
):
'''
Grants access to parent subsystems to the host specified by I{nqn}.
SEE ALSO
========
B{delete}
'''
self
.
parent
.
cfnode
.
add_allowed_host
(
nqn
)
UIAllowedHostNode
(
self
,
nqn
)
def
ui_complete_create
(
self
,
parameters
,
text
,
current_param
):
completions
=
[]
if
current_param
==
'
nqn
'
:
for
host
in
self
.
get_node
(
'
/hosts
'
).
children
:
completions
.
append
(
host
.
cfnode
.
nqn
)
if
len
(
completions
)
==
1
:
return
[
completions
[
0
]
+
'
'
]
else
:
return
completions
def
ui_command_delete
(
self
,
nqn
):
'''
Recursively deletes the namespace with the specified I{nsid}, and all
objects hanging under it.
SEE ALSO
========
B{create}
'''
self
.
parent
.
cfnode
.
remove_allowed_host
(
nqn
)
self
.
refresh
()
def
ui_complete_delete
(
self
,
parameters
,
text
,
current_param
):
completions
=
[]
if
current_param
==
'
nqn
'
:
for
nqn
in
self
.
parent
.
cfnode
.
allowed_hosts
:
completions
.
append
(
nqn
)
if
len
(
completions
)
==
1
:
return
[
completions
[
0
]
+
'
'
]
else
:
return
completions
class
UIAllowedHostNode
(
UINode
):
def
__init__
(
self
,
parent
,
nqn
):
UINode
.
__init__
(
self
,
nqn
,
parent
)
class
UIPortsNode
(
UINode
):
def
__init__
(
self
,
parent
):
UINode
.
__init__
(
self
,
'
ports
'
,
parent
)
...
...
@@ -320,6 +382,45 @@ class UIPortNode(UINode):
"
The Port could not be disabled.
"
)
class
UIHostsNode
(
UINode
):
def
__init__
(
self
,
parent
):
UINode
.
__init__
(
self
,
'
hosts
'
,
parent
)
def
refresh
(
self
):
self
.
_children
=
set
([])
for
host
in
self
.
parent
.
cfnode
.
hosts
:
UIHostNode
(
self
,
host
)
def
ui_command_create
(
self
,
nqn
):
'''
Creates a new NVMe host.
SEE ALSO
========
B{delete}
'''
host
=
nvme
.
Host
(
nqn
,
mode
=
'
create
'
)
UIHostNode
(
self
,
host
)
def
ui_command_delete
(
self
,
nqn
):
'''
Recursively deletes the NVMe Host with the specified I{nqn}, and all
objects hanging under it.
SEE ALSO
========
B{create}
'''
host
=
nvme
.
Host
(
nqn
,
mode
=
'
lookup
'
)
host
.
delete
()
self
.
refresh
()
class
UIHostNode
(
UINode
):
def
__init__
(
self
,
parent
,
cfnode
):
UINode
.
__init__
(
self
,
cfnode
.
nqn
,
parent
,
cfnode
)
def
usage
():
print
(
"
syntax: %s save [file_to_save_to]
"
%
sys
.
argv
[
0
])
print
(
"
%s restore [file_to_restore_from]
"
%
sys
.
argv
[
0
])
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment