blob: 73d8433c884c741630e49c95d854470e4ccee916 [file] [log] [blame]
Václav Kubernátc31bd602019-03-07 11:44:48 +01001/*
2 * Copyright (C) 2019 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Václav Kubernát <kubernat@cesnet.cz>
5 * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
6 *
7*/
8
9#include <libyang/Tree_Data.hpp>
10#include <mutex>
11extern "C" {
12#include <nc_client.h>
13}
14#include <sstream>
Václav Kubernátc31bd602019-03-07 11:44:48 +010015#include "netconf-client.h"
16
17namespace libnetconf {
18
19namespace impl {
20
21/** @short Initialization of the libnetconf2 library client
22
23Just a safe wrapper over nc_client_init and nc_client_destroy, really.
24*/
25class ClientInit {
26 ClientInit()
27 {
28 nc_client_init();
29 nc_verbosity(NC_VERB_DEBUG);
30 }
31
32 ~ClientInit()
33 {
34 nc_client_destroy();
35 }
36
37public:
38 static ClientInit& instance()
39 {
40 static ClientInit lib;
41 return lib;
42 }
43
44 ClientInit(const ClientInit&) = delete;
45 ClientInit(ClientInit&&) = delete;
46 ClientInit& operator=(const ClientInit&) = delete;
47 ClientInit& operator=(ClientInit&&) = delete;
48};
49
50static std::mutex clientOptions;
51
52inline void custom_free_nc_reply_data(nc_reply_data* reply)
53{
54 nc_reply_free(reinterpret_cast<nc_reply*>(reply));
55}
56inline void custom_free_nc_reply_error(nc_reply_error* reply)
57{
58 nc_reply_free(reinterpret_cast<nc_reply*>(reply));
59}
60
61template <auto fn>
62using deleter_from_fn = std::integral_constant<decltype(fn), fn>;
63
64template <typename T>
65struct deleter_type_for {
66 using func_type = void (*)(T*);
67};
68
69template <typename T>
70struct deleter_for {
71};
72
73template <>
74struct deleter_for<struct nc_rpc> {
75 static constexpr void (*free)(struct nc_rpc*) = deleter_from_fn<nc_rpc_free>();
76};
77template <>
78struct deleter_for<struct nc_reply> {
79 static constexpr void (*free)(struct nc_reply*) = deleter_from_fn<nc_reply_free>();
80};
81template <>
82struct deleter_for<struct nc_reply_data> {
83 static constexpr void (*free)(struct nc_reply_data*) = deleter_from_fn<custom_free_nc_reply_data>();
84};
85template <>
86struct deleter_for<struct nc_reply_error> {
87 static constexpr void (*free)(struct nc_reply_error*) = deleter_from_fn<custom_free_nc_reply_error>();
88};
89
90template <typename T>
91using unique_ptr_for = std::unique_ptr<T, decltype(deleter_for<T>::free)>;
92
93template <typename T>
94auto guarded(T* ptr)
95{
96 return unique_ptr_for<T>(ptr, deleter_for<T>::free);
97}
98
99unique_ptr_for<struct nc_reply> do_rpc(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
100{
101 uint64_t msgid;
102 NC_MSG_TYPE msgtype;
103
104 msgtype = nc_send_rpc(session->session_internal(), rpc.get(), 1000, &msgid);
105 if (msgtype == NC_MSG_ERROR) {
106 throw std::runtime_error{"Failed to send RPC"};
107 }
108 if (msgtype == NC_MSG_WOULDBLOCK) {
109 throw std::runtime_error{"Timeout sending an RPC"};
110 }
111
112 struct nc_reply* raw_reply;
113 while (true) {
114 msgtype = nc_recv_reply(session->session_internal(), rpc.get(), msgid, 20000, LYD_OPT_DESTRUCT | LYD_OPT_NOSIBLINGS, &raw_reply);
115 auto reply = guarded(raw_reply);
116 raw_reply = nullptr;
117
118 switch (msgtype) {
119 case NC_MSG_ERROR:
120 throw std::runtime_error{"Failed to receive an RPC reply"};
121 case NC_MSG_WOULDBLOCK:
122 throw std::runtime_error{"Timed out waiting for RPC reply"};
123 case NC_MSG_REPLY_ERR_MSGID:
124 throw std::runtime_error{"Received a wrong reply -- msgid mismatch"};
125 case NC_MSG_NOTIF:
126 continue;
127 default:
128 return reply;
129 }
130 }
131 __builtin_unreachable();
132}
133
134client::ReportedError make_error(unique_ptr_for<struct nc_reply>&& reply)
135{
136 if (reply->type != NC_RPL_ERROR) {
137 throw std::logic_error{"Cannot extract an error from a non-error server reply"};
138 }
139
140 auto errorReply = guarded(reinterpret_cast<struct nc_reply_error*>(reply.release()));
141
142 // TODO: capture the error details, not just that human-readable string
143 std::ostringstream ss;
144 ss << "Server error:" << std::endl;
145 for (uint32_t i = 0; i < errorReply->count; ++i) {
146 const auto e = errorReply->err[i];
147 ss << " #" << i << ": " << e.message;
148 if (e.path) {
149 ss << " (XPath " << e.path << ")";
150 }
151 ss << std::endl;
152 }
153 return client::ReportedError{ss.str()};
154}
155
156unique_ptr_for<struct nc_reply_data> do_rpc_data(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
157{
158 auto x = do_rpc(session, std::move(rpc));
159
160 switch (x->type) {
161 case NC_RPL_DATA:
162 return guarded(reinterpret_cast<struct nc_reply_data*>(x.release()));
163 case NC_RPL_OK:
164 throw std::runtime_error{"Received OK instead of a data reply"};
165 case NC_RPL_ERROR:
166 throw make_error(std::move(x));
167 default:
168 throw std::runtime_error{"Unhandled reply type"};
169 }
170}
171
172void do_rpc_ok(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
173{
174 auto x = do_rpc(session, std::move(rpc));
175
176 switch (x->type) {
177 case NC_RPL_DATA:
178 throw std::runtime_error{"Unexpected DATA reply"};
179 case NC_RPL_OK:
180 return;
181 case NC_RPL_ERROR:
182 throw make_error(std::move(x));
183 default:
184 throw std::runtime_error{"Unhandled reply type"};
185 }
186}
187}
188
189namespace client {
190
191struct nc_session* Session::session_internal()
192{
193 return m_session;
194}
195
196Session::Session(struct nc_session* session)
197 : m_session(session)
198{
199 impl::ClientInit::instance();
200}
201
202Session::~Session()
203{
204 ::nc_session_free(m_session, nullptr);
205}
206
207std::unique_ptr<Session> Session::connectPubkey(const std::string& host, const uint16_t port, const std::string& user, const std::string& pubPath, const std::string& privPath)
208{
209 impl::ClientInit::instance();
210
211 {
212 // FIXME: this is still horribly not enough. libnetconf *must* provide us with something better.
213 std::lock_guard lk(impl::clientOptions);
214 nc_client_ssh_set_username(user.c_str());
215 nc_client_ssh_set_auth_pref(NC_SSH_AUTH_PUBLICKEY, 5);
216 nc_client_ssh_add_keypair(pubPath.c_str(), privPath.c_str());
217 }
218 auto session = std::make_unique<Session>(nc_connect_ssh(host.c_str(), port, nullptr));
219 if (!session->m_session) {
220 throw std::runtime_error{"nc_connect_ssh failed"};
221 }
222 return session;
223}
224
225std::unique_ptr<Session> Session::connectSocket(const std::string& path)
226{
227 impl::ClientInit::instance();
228
229 auto session = std::make_unique<Session>(nc_connect_unix(path.c_str(), nullptr));
230 if (!session->m_session) {
231 throw std::runtime_error{"nc_connect_unix failed"};
232 }
233 return session;
234}
235
236std::vector<std::string_view> Session::capabilities() const
237{
238 std::vector<std::string_view> res;
239 auto caps = nc_session_get_cpblts(m_session);
240 while (*caps) {
241 res.emplace_back(*caps);
242 ++caps;
243 }
244 return res;
245}
246
247std::shared_ptr<libyang::Data_Node> Session::get(const std::string& filter)
248{
249 auto rpc = impl::guarded(nc_rpc_get(filter.c_str(), NC_WD_ALL, NC_PARAMTYPE_CONST));
250 if (!rpc) {
251 throw std::runtime_error("Cannot create get RPC");
252 }
253 auto reply = impl::do_rpc_data(this, std::move(rpc));
254 // TODO: can we do without copying?
255 // If we just default-construct a new node (or use the create_new_Data_Node) and then set reply->data to nullptr,
256 // there are mem leaks and even libnetconf2 complains loudly.
257 return libyang::create_new_Data_Node(reply->data)->dup_withsiblings(1);
258}
259
260std::string Session::getSchema(const std::string_view identifier, const std::optional<std::string_view> version)
261{
262 auto rpc = impl::guarded(nc_rpc_getschema(identifier.data(), version ? version.value().data() : nullptr, nullptr, NC_PARAMTYPE_CONST));
263 if (!rpc) {
264 throw std::runtime_error("Cannot create get-schema RPC");
265 }
266 auto reply = impl::do_rpc_data(this, std::move(rpc));
267
268 auto node = libyang::create_new_Data_Node(reply->data)->dup_withsiblings(1);
269 auto set = node->find_path("data");
270 for (auto node : set->data()) {
271 if (node->schema()->nodetype() == LYS_ANYXML) {
272 libyang::Data_Node_Anydata anydata(node);
273 return anydata.value().str;
274 }
275 }
276 throw std::runtime_error("Got a reply to get-schema, but it didn't contain the required schema");
277}
278
279std::shared_ptr<libyang::Data_Node> Session::getConfig(const NC_DATASTORE datastore, const std::optional<const std::string> filter)
280{
281 auto rpc = impl::guarded(nc_rpc_getconfig(datastore, filter ? filter->c_str() : nullptr, NC_WD_ALL, NC_PARAMTYPE_CONST));
282 if (!rpc) {
283 throw std::runtime_error("Cannot create get-config RPC");
284 }
285 auto reply = impl::do_rpc_data(this, std::move(rpc));
286 // TODO: can we do without copying?
287 // If we just default-construct a new node (or use the create_new_Data_Node) and then set reply->data to nullptr,
288 // there are mem leaks and even libnetconf2 complains loudly.
289 auto dataNode = libyang::create_new_Data_Node(reply->data);
290 if (!dataNode)
291 return nullptr;
292 else
293 return dataNode->dup_withsiblings(1);
294}
295
296void Session::editConfig(const NC_DATASTORE datastore,
297 const NC_RPC_EDIT_DFLTOP defaultOperation,
298 const NC_RPC_EDIT_TESTOPT testOption,
299 const NC_RPC_EDIT_ERROPT errorOption,
300 const std::string& data)
301{
302 auto rpc = impl::guarded(nc_rpc_edit(datastore, defaultOperation, testOption, errorOption, data.c_str(), NC_PARAMTYPE_CONST));
303 if (!rpc) {
304 throw std::runtime_error("Cannot create edit-config RPC");
305 }
306 impl::do_rpc_ok(this, std::move(rpc));
307}
308
309void Session::copyConfigFromString(const NC_DATASTORE target, const std::string& data)
310{
311 auto rpc = impl::guarded(nc_rpc_copy(target, nullptr, target /* yeah, cannot be 0... */, data.c_str(), NC_WD_UNKNOWN, NC_PARAMTYPE_CONST));
312 if (!rpc) {
313 throw std::runtime_error("Cannot create copy-config RPC");
314 }
315 impl::do_rpc_ok(this, std::move(rpc));
316}
317
318void Session::commit()
319{
320 auto rpc = impl::guarded(nc_rpc_commit(1, /* "Optional confirm timeout" how do you optional an uint32_t? */ 0, nullptr, nullptr, NC_PARAMTYPE_CONST));
321 if (!rpc) {
322 throw std::runtime_error("Cannot create commit RPC");
323 }
324 impl::do_rpc_ok(this, std::move(rpc));
325}
326
327void Session::discard()
328{
329 auto rpc = impl::guarded(nc_rpc_discard());
330 if (!rpc) {
331 throw std::runtime_error("Cannot create discard RPC");
332 }
333 impl::do_rpc_ok(this, std::move(rpc));
334}
335
336ReportedError::ReportedError(const std::string& what)
337 : std::runtime_error(what)
338{
339}
340
341ReportedError::~ReportedError() = default;
342}
343}